Asking ChatGPT to build a YouTube Download App for me.

Asking ChatGPT to build a YouTube Download App for me.

So I have started seeing lots of people starting to publish videos where they ask generative AI to generate the code for an entire app and fairly complex apps are getting generated.

Thought I will try it out myself as well.

The cover image is generated using AlphaCTR.com

The Prompt

So the idea is simple. I want to make a GUI wrapper for yt-dlp - a tool I frequently use to download videos (or parts thereof) from YouTube.

I asked ChatGPT (GPT4) to build this app for me.

So here was my prompt. (It is fairly detailed).

I want to make a Flutter app that downloads videos using the 'yt-dlp' tool. 

Assume the following 
1. I do not need to setup yt-dlp, it exists on my PC already. 
2. Do not help me with Flutter installation and plugin adding steps. 
3. Only show the code for the lib/main.dart file 

The app's UI will have the following 

1. Title bar: "YouTube Downloader GUI" 
2. Input Box [id: ytUrl, placeholder: "youtube video url"]
3. A row of 3 checkboxes 
  a. "Video" [id: dlVid, default: checked]
  b. "Audio" [id: dlAud, default: unchecked] 
  c. "Thumbnail" [id: dlThumb, default: unchecked]
4. Two input boxes for start and end time 
  a. "Start time" [id: timeStart] 
  b. "End Time" [id: timeEnd] 
5. Two dropdown boxes to select quality of audio and video 
  a. id: qualVid, choices: [480p, 720p, 1080p, 2K, 4K], default: 1080p 
  b.  id: qualAud, choices: [64k, 128k, 192k, 256k, 320k] default: 192k 


The logic for downloading should be this. 

1. If dlAud = true, download audio separately 
2. If dlThumb = true, download thumbnail too 
3. If start and end time are given, download only that portion of the video 
4. If ytUrl is not provided or doesn't contain 'youtube.com' in it show a Dialog "Enter valid YouTube URL"
5. Factor in the qualVid and qualAud when downloading audio and video. 
6. Download videos in mp4 form and audio in mp3 form

Highlighting a few things here

  1. I explicitly asked only to generate the lib/main.dart code for me because otherwise ChatGPT by default provides all the info required to setup a Flutter project and install dependencies. Since I am well versed with Flutter at a basic level to do these things myself, I asked it to avoid it for faster output

  2. I am building a Flutter app (and not say, React Native Desktop) especially because this is a platform I am slightly familiar with and I can debug the final output

The Initial Output

So here is the code that ChatGPT generated for me

import 'package:flutter/material.dart';
import 'package:process_run/cmd_run.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'YouTube Downloader GUI',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String ytUrl;
  bool dlVid = true;
  bool dlAud = false;
  bool dlThumb = false;
  TimeOfDay timeStart;
  TimeOfDay timeEnd;
  String qualVid = '1080p';
  String qualAud = '192k';

  List<String> qualVidOptions = ['480p', '720p', '1080p', '2K', '4K'];
  List<String> qualAudOptions = ['64k', '128k', '192k', '256k', '320k'];

  void download() async {
    if (ytUrl == null || !ytUrl.contains('youtube.com')) {
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          content: Text('Enter valid YouTube URL'),
        ),
      );
      return;
    }

    String command = 'yt-dlp -f \'bestvideo[height<=${qualVid.replaceAll('p', '')}]'
        '+bestaudio/best[height<=${qualVid.replaceAll('p', '')}]\' $ytUrl';
    if (dlAud) command += ' -x --audio-format mp3 --audio-quality ${qualAud}';
    if (dlThumb) command += ' --write-thumbnail';
    if (timeStart != null && timeEnd != null)
      command +=
          ' --postprocessor-args "-ss ${timeStart.format(context)} -to ${timeEnd.format(context)}"';

    await runCmd(ProcessCmd(command.split(' ')[0], command.split(' ').sublist(1)));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('YouTube Downloader GUI'),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          children: <Widget>[
            TextField(
              onChanged: (value) => ytUrl = value,
              decoration: InputDecoration(
                labelText: 'YouTube Video URL',
              ),
            ),
            Row(
              children: <Widget>[
                Checkbox(
                  value: dlVid,
                  onChanged: (bool value) {
                    setState(() {
                      dlVid = value;
                    });
                  },
                ),
                Text('Video'),
                Checkbox(
                  value: dlAud,
                  onChanged: (bool value) {
                    setState(() {
                      dlAud = value;
                    });
                  },
                ),
                Text('Audio'),
                Checkbox(
                  value: dlThumb,
                  onChanged: (bool value) {
                    setState(() {
                      dlThumb = value;
                    });
                  },
                ),
                Text('Thumbnail'),
              ],
            ),
            Row(
              children: [
                Expanded(
                  child: TextField(
                    onTap: () async {
                      TimeOfDay time = await showTimePicker(
                        context: context,
                        initialTime: TimeOfDay.now(),
                      );
                      if (time != null) setState(() => timeStart = time);
                    },
                    readOnly: true,
                    decoration: InputDecoration(
                      labelText: 'Start Time',
                      hintText: timeStart?.format(context) ?? '',
                    ),
                  ),
                ),
                SizedBox(width: 16),
                Expanded(
                  child: TextField(
                    onTap: () async {
                      TimeOfDay time = await showTimePicker(
                        context: context,
                        initialTime: TimeOfDay.now(),
                      );
                      if (time != null) setState(() => timeEnd = time);
                    },
                    readOnly: true,
                    decoration: InputDecoration(
                      labelText: 'End Time',
                      hintText: timeEnd?.format(context) ?? '',
                    ),
                  ),
                ),
              ],
            ),
            DropdownButton(
              value: qualVid,
              items: qualVidOptions
                  .map((value) => DropdownMenuItem(
                        child: Text(value),
                        value: value,
                      ))
                  .toList(),
              onChanged: (String value) {
                setState(() {
                  qualVid = value;
                });
              },
            ),
            DropdownButton(
              value: qualAud,
              items: qualAudOptions
                  .map((value) => DropdownMenuItem(
                        child: Text(value),
                        value: value,
                      ))
                  .toList(),
              onChanged: (String value) {
                setState(() {
                  qualAud = value;
                });
              },
            ),
            ElevatedButton(
              onPressed: download,
              child: Text('Download'),
            ),
          ],
        ),
      ),
    );
  }
}

It does also mention that this might not cover all corner cases and be mindful of YouTube policies

Trying to use it

Let's see what happens when I try to use it. I am not sure if this code will work straight out of the box or not!

So it didn't work out of the box, and the main reason for that is that ChatGPT's knowledge cutoff date is Sept 2021, which means most things in Flutter and Dart that have evolved since then wouldn't have found its way into ChatGPT's codex. One particular thing that affects is that ChatGPT-generated Flutter code doesn't use const constructors. Because it would not have been trained on much code with const

Here is a full diff of the changes I had to do to get it just working.

https://github.com/championswimmer/yt_dlp_ui/commit/8a1fe9ba3a2bb33120779138b66222c804a25859

Some highlights

  1. ofcourse, lots of const additions

  1. not understanding 2023-era Dart null safety

  1. using some older APIs of the process_run library

Getting it to work

Had to make one final change (which ChatGPT already had warned me about) - to disable MacOS sandbox mode, to be able to run shelle commands

Basically go to macos/Runner/DebugProfile.entitlements and

    <key>com.apple.security.app-sandbox</key>
    <false/>

Mostly after these changes, it basically works!

  • The start/end time input boxes are timepickers, which we need to fix!

  • I stopped the download midway that's why it didn't finish

  • Will need to fix how the video/audio quality is translated in the shell command

Conclusion

I think this is pretty good. Obviously the way this will get weaponised won't be us talking to a damn chatbot and asking it to code. There will be better ways this will integrate into the coding workflow.

I will next try to achieve the same using smol-dev-js which is based on smol.ai and I'll probably write up about that when I do.

Till then, this is fairly promising. And I will finally turn the Small App Ideas.md page from by Obsidian into actual apps maybe!