diff --git a/examples/kodi_music_videos_config.yaml b/examples/kodi_music_videos_config.yaml new file mode 100644 index 00000000..b0506938 --- /dev/null +++ b/examples/kodi_music_videos_config.yaml @@ -0,0 +1,67 @@ +# This example shows how to download and format a Youtube video OR playlist +# to display in Kodi as a music video. Kodi requires music videos to be in +# a shared directory, so we will configure this to make the output directory +# formatted as: +# +# /path/to/Music Videos +# Elton John - Rocketman.jpg +# Elton John - Rocketman.mp4 +# Elton John - Rocketman.nfo +# System of a Down - Chop Suey.jpg +# System of a Down - Chop Suey.mp4 +# System of a Down - Chop Suey.nfo +# ... +# +configuration: + working_directory: '.ytdl-sub-downloads' + +presets: + yt_music_video: + # Youtube playlists are our source/download strategy. However, this + # can be overwritten to download music videos from a 'channel' or a + # single 'video' + youtube: + download_strategy: "playlist" + + # For advanced YTDL users only. + # It is wise to leave ignoreerrors=True to avoid errors for things + # like age-restricted videos in case you have not set your cookie. + ytdl_options: + ignoreerrors: True + + # For each video downloaded, set the file and thumbnail name here. + # We set both with {music_video_name}, which is a variable we define in + # the overrides section further below to represent consistent naming format. + # + # Another field worth mentioning is maintain_download_archive=True. This + # is generally a good thing to enable with playlists because it will + # store previously downloaded video ids to tell YTDL not to re-download + # them on a successive invocation. + output_options: + output_directory: "path/to/Music Videos" + file_name: "{music_video_name}.{ext}" + thumbnail_name: "{music_video_name}.jpg" + maintain_download_archive: True + + # Always convert the video thumbnail to jpg + # TODO: always convert all thumbnails to jpg in code + convert_thumbnail: + to: "jpg" + + # For each video downloaded, add a music video NFO file for it. Populate it + # with tags that Kodi will read and use to display it in the music or music + # videos section. + nfo_tags: + nfo_name: "{music_video_name}.nfo" + nfo_root: "musicvideo" + tags: + artist: "{artist}" + title: "{title}" + album: "Music Videos" + year: "{upload_year}" + + # Overrides is a section where we can define our own variables, and use them in + # any other section. We define our episode file name here, which gets reused above + # for the video, thumbnail, and NFO file. + overrides: + music_video_name: "{sanitized_artist} - {sanitized_title}" diff --git a/examples/kodi_music_videos_subscriptions.yaml b/examples/kodi_music_videos_subscriptions.yaml new file mode 100644 index 00000000..e937e6bb --- /dev/null +++ b/examples/kodi_music_videos_subscriptions.yaml @@ -0,0 +1,84 @@ +# This example shows how we can use the `kodi_music_videos_config.yaml` preset +# to download music videos in a few different ways. We will use made-up artists +# in each example + +############################################################################### +# LEVEL 1 - DOWNLOAD MUSIC VIDEO PLAYLIST + +# Subscription names are defined by you. We will call this one john_smith +# for simplicity, and it will download every single video in john_smith's music +# video playlist. Many artists maintain a playlist of all their music videos, +# which makes this an easy way to grab all of them. +john_smith: + # We must define a preset to use from our config. We named the one in the + # config example "yt_music_video", so set that here. + preset: "yt_music_video" + + # Since our preset download strategy is set to 'playlist', set the playlist id. + youtube: + playlist_id: "UCsvn_Po0SmunchJYtttWpOxMg" + + # Overrides can be defined per-subscription. If you noticed, we used {artist} + # and {sanitized_artist} in our "yt_music_video" preset. We intended to reserve + # that variable to be defined for each individual subscription. Each override + # defined here will create a 'sanitized_' version that is safe for file systems. + # + # A note for Kodi music videos, it is important to make sure your artist name + # exactly matches how it is formatted in Kodi itself, otherwise it will be + # read in as a new artist + overrides: + artist: "John Smith and the Instrument Players" + +############################################################################### +# LEVEL 2 - DOWNLOAD SINGLE MUSIC VIDEO + +# It is not always ideal to download all of an artist's music videos. +# Maybe you only like one song of theirs. We can reuse our preset +# to download a single video instead. +# +# The only difference between this example and the one above is +# - youtube.after +# This is saying 'only download videos in the last 14 days' +# - ytdl_options.break_on_reject +# This is getting into YTDL voodoo. By default, YTDL will try to +# download all channel videos beginning with the most recent one. +# By setting break_on_reject, we will break this full-download on +# the first video that gets rejected. Since we have youtube.after +# defined, all videos after today-14days will be rejected. Therefore, +# the first video that is out of range that it tries to download, it +# will stop there, and save a significant amount of time. +john_smith_one_hit_wonder: + preset: "yt_channel_as_tv" + youtube: + download_strategy: "video" + video_id: "QhY6r6oAErg" + overrides: + tv_show_name: "John /\ Smith" + ytdl_options: + break_on_reject: True + +############################################################################### +# LEVEL 3 - ROLLING ARCHIVE + +# If you put the recent archive example in a cron job, then in a year or so +# you will basically be datahoarding that channel unless you manually delete +# old videos. We automate because we are lazy. This example shows how to +# only keep the last 14-days worth of videos, and delete the rest. +# +# The only difference between this example and the one above is +# - output_options.keep_files.after +# This is saying "only keep files if they were uploaded in the last 14 days". +# All other files that this subscription had previously downloaded will be +# deleted. +john_smith_rolling_archive: + preset: "yt_channel_as_tv" + youtube: + channel_id: "UCsvn_Po0SmunchJYtttWpOxMg" + after: today-14days + overrides: + tv_show_name: "John /\ Smith" + ytdl_options: + break_on_reject: True + output_options: + keep_files: + after: today-14days \ No newline at end of file diff --git a/src/ytdl_sub/entries/variables/youtube_variables.py b/src/ytdl_sub/entries/variables/youtube_variables.py index 49247ce7..bdace313 100644 --- a/src/ytdl_sub/entries/variables/youtube_variables.py +++ b/src/ytdl_sub/entries/variables/youtube_variables.py @@ -18,3 +18,22 @@ class YoutubeVideoVariables(EntryVariables): return self.kwargs("track") return super().title + + @property + def playlist_index(self) -> int: + """ + Returns + ------- + The index of the video in the playlist. For non-playlist download strategies, this will + always return 1. + """ + return 1 + + @property + def playlist_size(self) -> int: + """ + Returns + ------- + The size of the playlist. For non-playlist download strategies, this will always return 1. + """ + return 1 diff --git a/src/ytdl_sub/entries/youtube.py b/src/ytdl_sub/entries/youtube.py index da5b39e7..35b2b0da 100644 --- a/src/ytdl_sub/entries/youtube.py +++ b/src/ytdl_sub/entries/youtube.py @@ -1,6 +1,8 @@ import os.path from pathlib import Path +from typing import List, Dict +from ytdl_sub.entries.base_entry import PlaylistMetadata from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.variables.youtube_variables import YoutubeVideoVariables @@ -31,3 +33,92 @@ class YoutubeVideo(YoutubeVideoVariables, Entry): return possible_thumbnail_path return super().get_download_thumbnail_path() + + +class YoutubePlaylistVideo(YoutubeVideo): + def __init__( + self, + entry_dict: Dict, + working_directory: str, + playlist_metadata: PlaylistMetadata, + ): + """ + Initialize the playlist video with playlist metadata + """ + super().__init__(entry_dict=entry_dict, working_directory=working_directory) + self._playlist_metadata = playlist_metadata + + @classmethod + def from_youtube_video( + cls, + youtube_video: YoutubeVideo, + playlist_metadata: PlaylistMetadata, + ) -> "YoutubePlaylistVideo": + """ + Parameters + ---------- + youtube_video: + Video to convert to an playlist video + playlist_metadata: + Metadata for playlist ordering + + Returns + ------- + YoutubeVideo converted to a YoutubePlaylistVideo + """ + return YoutubePlaylistVideo( + entry_dict=youtube_video._kwargs, # pylint: disable=protected-access + working_directory=youtube_video.working_directory(), + playlist_metadata=playlist_metadata, + ) + + @property + def playlist_index(self) -> int: + """ + Returns + ------- + The playlist index + """ + return self._playlist_metadata.playlist_index + + @property + def playlist_size(self) -> int: + """ + Returns + ------- + The size of the playlist + """ + return self._playlist_metadata.playlist_count + + +class YoutubePlaylist(Entry): + @property + def _videos(self) -> List[YoutubeVideo]: + """ + Returns all videos in the playlist represented by non-playlist Videos. Use this to fetch any + data needed from the videos before representing it as a playlist video. + """ + return [ + YoutubeVideo(entry_dict=entry, working_directory=self._working_directory) + for entry in self.kwargs("entries") + ] + + def playlist_videos(self) -> List[YoutubePlaylistVideo]: + """ + Returns + ------- + All videos in the playlist represented as YoutubePlaylistVideos. This updates + playlist-specific fields like playlist_index and playlist_size with its actual value. + """ + return [ + YoutubePlaylistVideo.from_youtube_video( + youtube_video=video, + playlist_metadata=PlaylistMetadata( + playlist_id=self.uid, + playlist_extractor=self.extractor, + playlist_index=video.kwargs("playlist_index"), + playlist_count=self.kwargs('playlist_count'), + ), + ) + for video in self._videos + ]