youtube-dlyt-dlp

Set metadata based on the output filename in yt-dlp


I can extract audio from video:

yt-dlp -x --audio-format vorbis --audio-quality 256k --embed-thumbnail -P ~/Music -o "John Doe - Cool song.%(ext)s" tubelink.kom

Now, I would like to embed metadata based on the filename format output, possible? In my example code would be artist John Doe and Song name Cool song. Thanks.


Solution

  • I mostly use yt-dlp for saving offline copies of technology tutorials and the like, so might need to adjust for your use-case. I like capturing a lot of non-standard metrics like # likes / # views / the date I downloaded the video, as I find this info useful when I've downloaded 4 or 5 tutorials and want to see which one had been the most popular at the time I downloaded them (helps me to pick the first one to watch). The link in the other answer is a very good reference. But since the (currently only) other answer didn't attempt to explain details very thoroughly or provide any concrete examples, I decided to attempt to add one based on my own experiences as well.

    I use a large wrapper function and a lot of this is actually controlled by variables/function arguments in my setup but here is an example of the command that gets generated if I want to download a particular video format in 480p (generally on screen text commands become difficult to read at lower resolutions and high resolutions eat up more disk space).

    This is from a bash shell function on Linux but aside from how I use the variable, everything in yt-dlp itself should be cross-platform. Obviously, you don't need all the options I am using; I just wanted to provide a complete example. The relevant options are explained below the snippet.

    local downloadTimestamp="$(date +'%F %T %Z')";
    # double-escape any colons ONCE to prevent them being interpreted by --parse-metadata
    downloadTimestamp="${downloadTimestamp//:/\\:}";
    
    yt-dlp -f "bestvideo+bestaudio/best" \
      --format-sort res:480,+size,+br,codec  \
      -o "%(uploader)s_-_%(title)s.%(ext)s" \
      --parse-metadata "${downloadTimestamp}:%(meta_download_date)s" \
      --parse-metadata "%(like_count)s:%(meta_likes)s" \
      --parse-metadata "%(dislike_count)s:%(meta_dislikes)s" \
      --parse-metadata "%(view_count)s:%(meta_views)s" \
      --parse-metadata "%(average_rating)s:%(meta_rating)s" \
      --parse-metadata "%(release_date>%Y-%m-%d,upload_date>%Y-%m-%d)s:%(meta_publish_date)s" \
      --restrict-filenames --windows-filenames \
      --quiet --no-warnings \
      --ignore-errors --prefer-free-formats \
      --xattrs --no-overwrites \
      --sub-lang en --embed-subs --add-metadata --merge-output-format mkv \
      --write-auto-subs --embed-metadata --embed-thumbnail \
      "${url}"
    

    --parse-metadata has a TO:FROM argument syntax. Despite the name not being the most intuitive if your goal is to capture some kind of metadata from the page and have it embedded in the downloaded file, this is the most relevant option that you need to use for mapping values. The --embed-metadata option is also important for having the metadata embedded into the audio/video file rather than as a 2nd file.

    The TO part of the --parse-metadata argument can be a string literal or some text that you expand from a variable in a script. Or it can be another yt-dlp format string like %(artist)s or %(title)s (Note: the format strings used here are the SAME ones that you use for creating the output template. The full list can be found here - look for the text "The available fields are"). The format string can also be customized like %(release_date>%Y-%m-%d)s. You can even have a custom format string that falls back to secondary field if the first field is empty (e.g. %(release_date>%Y-%m-%d,upload_date>%Y-%m-%d)s). If you are adding string literals or values from variables, it is important to make sure that either no colons (:) are present or else to escape any colons that are intended as part of the value using backslashes as I have done with the downloadTimestamp in the above. For example:

    --parse-metadata "Tutorial\\:Intro to parse-metadata:%(meta_dummy)s"
    

    or

    --parse-metadata 'Foo\:Bar:%(meta_dummy)s'
    

    The FROM part of the --parse-metadata argument is basically the word "meta" followed by an underscore and the name of an existing or new metadata field. The field name is specified as lowercase in --parse-metadata but seems to be created in all caps in the actual file. All of the fields I am referencing in the above snippet are custom fields that I made up.

    I could then view them in the downloaded file later using mediainfo or similar tools. For example, on Linux, I can do this (note: mediainfo is cross-platform and there is a GUI for it if you are not comfortable with terminal):

    mediainfo --Language=raw --Full --Inform="General;%LIKES%" "${filePath}"
    # 3
         
    mediainfo --Language=raw --Full --Inform="General;%VIEWS%" "${filePath}"
    # 95
         
    mediainfo --Language=raw --Full --Inform="General;%PUBLISH_DATE%" "${filePath}"
    # 2022-04-05
         
    mediainfo --Language=raw --Full --Inform="General;%DISLIKES%" "${filePath}"
    # NA
    

    You would get NA for the DISLIKES field if it was never captured. This could happen for instance if you get a video from a site with no rating/dislike system. Or, for youtube specifically, if you tried to capture that metadata for a channel/video that doesn't enable dislikes (which is the default these days).


    Update/Bonus:

    If you plan on making a separate "prefetch" call for metadata, this might also be useful. This is related to the above mostly in that you can use it to build out more complex values, where doing it in the normal --parse-metadata syntax would either be extremely messy or otherwise hard to do in a single line. It's totally not a shameless addendum I'm using to help myself remember :-)

    You can fetch metadata in several ways and either print it to stdout/terminal/whatever (e.g. using --print) or save it directly to a json file and extract from the file (e.g. using --write-info-json).

    I feel like --write-info-json is the better documented of the two approaches but also has the downside of pulling back more info than most people will typically want. It is a great starting point to get a sample with most of the fields though (most of the single value fields have the same names as the --parse-metadata / found output-template properties but I'm not sure all of the JSON key names are explicitly documented - in any case, it can be handy to have a reference file).

    There are some comments in the yt-dlp github issue tracker that give good hints for anyone looking to print the same data without using a file (I found this issue in particular to be very helpful for instance). Here's a sample I made that builds on the examples there and prints out a custom JSON string with most of the general / non-format specific properties possible without saving anything to disk. You'll notice that I added a bunch of options related to the filename. Those have an effect on the filename field that gets printed but if you aren't interested in that property or have different requirements, then they probably won't be very useful. More than likely, you would probably want the full file instead but there may be some scenarios where you want just select fields without saving to file / printing the entire JSON (maybe a setup where you don't want any .info.json file in the same folder and only need a small subset of the data for example).

        yt-dlp --no-download \
          --ignore-no-formats-error \
          --restrict-filenames \
          --windows-filenames \
          --trim-filenames 140 \
          -o '%(uploader)s_-_%(title)s.%(ext)s' \
          --print "%(.{id,title,description,language,filename,_has_drm,age_limit,album,album_artist,alt_title,artist,artists,average_rating,categories,channel,channel_follower_count,channel_url,chapter,chapter_id,chapter_number,comment_count,composer,composers,creator,creators,dislike_count,duration,duration_string,epoch,fulltitle,genre,genres,license,like_count,playlist,playlist_count,playlist_index,playlist_title,playlist_uploader,playlist_uploader_id,release_date,release_timestamp,release_year,tags,timestamp,track,track_id,track_number,upload_date,uploader,uploader_url,view_count,comments})#j" \
          "${url}"
    

    The core part of this is the 2nd to last line. The --print is instructing yt-dlp to print to stdout aka your terminal rather than saving to a file. The string that follows is a bit long in the example above but most of that is just a comma-separated list of key names. The syntax spec can be found under the Output Template section of the documentation:

    Object traversal: The dictionaries and lists available in metadata can be traversed by using a dot . separator; e.g. %(tags.0)s, %(subtitles.en.-1.ext)s. You can do Python slicing with colon :; E.g. %(id.3:7)s, %(id.6:2:-1)s, %(formats.:.format_id)s. Curly braces {} can be used to build dictionaries with only specific keys; e.g. %(formats.:.{format_id,height})#j. An empty field name %()s refers to the entire infodict; e.g. %(.{id,title})s. Note that all the fields that become available using this method are not listed below. Use -j to see such fields

    So as illustrated in my original write-up, %(field)s instructs to capture a string. But %(.{listoffields})j instructs to work with JSON instead. You'll notice we actually also have a # symbol in there (e.g. %(.{listoffields})j#), which is covered a few points further down in the documentation. Basically, %(listoffields)j gives you raw / unformatted JSON, while %(listoffields)#j gives you formatted / pretty-print JSON.

    If you run the above against youtube urls, you'll probably also notice that some fields such as artist or comments aren't actually returned in the resulting JSON. Most of these are things that are selectively processed by yt-dlp depending on what data is provided / content type / site /etc. So if I run the same command on Bandcamp instead of Youtube, I would likely see the artist and track fields in the output even if those same fields are missing when run against Youtube.

    For comments, adding the --get-comments option would cause those to also be appended to the JSON. Depending on the number of comments, this can increase the size of the JSON a good bit (there are some related options for helping to control this or how comments are sorted. There are also some related extractor-specific arguments (e.g. options mostly limited to youtube with a couple of exceptions; see docs for max_comments / comment_sort and be aware that only a few non-Youtube sites support these). These are also passed to yt-dlp a bit different than normal args (e.g. --extractor-args "youtube:player-max_comments=25,5,25,5;comment_sort=top")

    These are explained more thoroughly in the Extractor Arguments section of the documentation under the youtube sub-section:

    • comment_sort: top or new (default) - choose comment sorting mode (on YouTube's side)

    • max_comments: Limit the amount of comments to gather. Comma-separated list of integers representing max-comments,max-parents,max-replies,max-replies-per-thread. Default is all,all,all,all

      E.g. all,all,1000,10 will get a maximum of 1000 replies total, with up to 10 replies per thread. 1000,all,100 will get a maximum of 1000 comments, with a maximum of 100 replies total

    From here, you should be able to extract values from the JSON data (assuming you captured it into a variable / printed to file / etc) and build out whatever crazy and complex data you can think up. The main things you'll want to watch out for are:

    1. In stricter scripting/programming languages, you'll want to first check that a field actually exists in the JSON since like I mentioned above, you won't always just get an empty value - sometimes the property itself can be omitted from the JSON.
    2. If you plan to construct custom strings/etc and use them as literals for --parse-metadata in another yt-dlp call, don't forget to escape the colon (:) character with a backslash.