Add download option

This commit is contained in:
Maks Snegov 2024-02-04 23:18:51 -08:00
parent 080842758d
commit a352f3cac0
3 changed files with 78 additions and 21 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ __pycache__/
client_secrets_file.json client_secrets_file.json
token.json token.json
playlists.csv playlists.csv
db.json

96
main.py
View File

@ -9,8 +9,14 @@ from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build from googleapiclient.discovery import build
from googleapiclient.errors import HttpError from googleapiclient.errors import HttpError
from tinydb import TinyDB, Query
from yt_dlp import YoutubeDL
_playlists = {} _playlists = {}
# output template: CHANNEL/DATE_TITLE_[ID].ext
DEFAULT_OUTPUT_TMPL = "%(channel)s/%(upload_date)s_%(title)s_[%(id)s].%(ext)s"
# download video 1080p or lower with audio
DEFAULT_FORMAT = "bestvideo[height<=1080]+bestaudio/best[height<=1080]"
def _truncate_title(title: str, length: int = 30) -> str: def _truncate_title(title: str, length: int = 30) -> str:
@ -170,13 +176,13 @@ def remove_video_from_playlist(yt_api, plitem_id: str, playlist_id: str,
def copy_playlist_items(yt_api, def copy_playlist_items(yt_api,
src_playlist: str, src_playlist_id: str,
dst_playlist: str, dst_playlist_id: str,
delete_from_src: bool = False, delete_from_src: bool = False,
limit: int = -1, limit: int = -1,
dry_run: bool = False): dry_run: bool = False):
src_playlist_items = list_playlist(yt_api, src_playlist) src_playlist_items = list_playlist(yt_api, src_playlist_id)
dst_playlist_items = list_playlist(yt_api, dst_playlist) dst_playlist_items = list_playlist(yt_api, dst_playlist_id)
dst_videos = {pl_item['snippet']['resourceId']['videoId'] dst_videos = {pl_item['snippet']['resourceId']['videoId']
for pl_item in dst_playlist_items} for pl_item in dst_playlist_items}
@ -188,10 +194,10 @@ def copy_playlist_items(yt_api,
was_processed = False was_processed = False
video_id = src_pl_item['snippet']['resourceId']['videoId'] video_id = src_pl_item['snippet']['resourceId']['videoId']
if video_id not in dst_videos: if video_id not in dst_videos:
add_video_to_playlist(yt_api, video_id, dst_playlist, dry_run) add_video_to_playlist(yt_api, video_id, dst_playlist_id, dry_run)
was_processed = True was_processed = True
if delete_from_src: if delete_from_src:
remove_video_from_playlist(yt_api, src_pl_item["id"], src_playlist, dry_run) remove_video_from_playlist(yt_api, src_pl_item["id"], src_playlist_id, dry_run)
was_processed = True was_processed = True
if was_processed: if was_processed:
processed_amt += 1 processed_amt += 1
@ -199,8 +205,9 @@ def copy_playlist_items(yt_api,
def get_video_info(youtube, video_id: str): def get_video_info(youtube, video_id: str):
try: try:
# TODO maybe remove 'status'
response = youtube.videos().list( response = youtube.videos().list(
part="snippet", part="localizations,snippet,contentDetails,statistics,status,topicDetails",
id=video_id id=video_id
).execute() ).execute()
except HttpError as e: except HttpError as e:
@ -216,7 +223,7 @@ def get_video_info(youtube, video_id: str):
def get_playlistitem_info(youtube, playlistitem_id: str): def get_playlistitem_info(youtube, playlistitem_id: str):
try: try:
response = youtube.playlistItems().list( response = youtube.playlistItems().list(
part="snippet", part="snippet,contentDetails",
id=playlistitem_id id=playlistitem_id
).execute() ).execute()
except HttpError as e: except HttpError as e:
@ -262,6 +269,14 @@ def main():
parser_dups.add_argument('-l', '--limit', type=int, default=-1, parser_dups.add_argument('-l', '--limit', type=int, default=-1,
help='Limit number of videos to process') help='Limit number of videos to process')
parser_download = subparsers.add_parser('download', help='Download videos from a playlist')
parser_download.add_argument('playlist', help='Playlist name/ID')
parser_download.add_argument('dst_folder', help='Destination folder')
parser_download.add_argument('-l', '--limit', type=int, default=-1,
help='Limit number of videos to process')
parser_download.add_argument('-r', '--remove-from-playlist', action='store_true',
help='Remove downloaded videos from the playlist')
args = parser.parse_args() args = parser.parse_args()
# Disable OAuthlib's HTTPS verification when running locally. # Disable OAuthlib's HTTPS verification when running locally.
@ -276,19 +291,16 @@ def main():
parser.print_help() parser.print_help()
return 1 return 1
elif args.command == 'copy': elif args.command in ('copy', 'move'):
src_playlist = get_playlist_id(args.src_playlist) delete_from_src = args.command == 'move'
dst_playlist = get_playlist_id(args.dst_playlist) copy_playlist_items(
copy_playlist_items(youtube, src_playlist, dst_playlist, youtube,
delete_from_src=False, get_playlist_id(args.src_playlist),
limit=args.limit, dry_run=args.dry_run) get_playlist_id(args.dst_playlist),
delete_from_src=delete_from_src,
elif args.command == 'move': limit=args.limit,
src_playlist = get_playlist_id(args.src_playlist) dry_run=args.dry_run
dst_playlist = get_playlist_id(args.dst_playlist) )
copy_playlist_items(youtube, src_playlist, dst_playlist,
delete_from_src=True,
limit=args.limit, dry_run=args.dry_run)
elif args.command == 'add': elif args.command == 'add':
playlist_id = get_playlist_id(args.playlist) playlist_id = get_playlist_id(args.playlist)
@ -339,6 +351,48 @@ def main():
else: else:
plitems_processed.add(video_id) plitems_processed.add(video_id)
elif args.command == "download":
db = TinyDB('db.json')
query = Query()
ydl_opts = {
'outtmpl': os.path.join(args.dst_folder, DEFAULT_OUTPUT_TMPL),
'format': DEFAULT_FORMAT,
}
# load playlist items
playlist_id = get_playlist_id(args.playlist)
plitems = list_playlist(youtube, playlist_id)
# limit number of videos to process
if args.limit > 0:
plitems = plitems[:args.limit]
for plitem in plitems:
video_id = plitem["snippet"]["resourceId"]["videoId"]
# skip if video is already in the database
if db.search(query.id == video_id):
continue
video_info = get_video_info(youtube, video_id)
# skip if video is not found on YouTube
if not video_info:
continue
video_title = _truncate_title(video_info['snippet']['title'])
if args.dry_run:
print(f"Would download video '{video_title}' [{video_id}]"
f" from playlist {args.playlist} to folder {args.dst_folder}")
else:
# download video
with YoutubeDL(ydl_opts) as ydl:
ydl.download(['https://www.youtube.com/watch?v=' + video_id])
db.insert(video_info)
# remove video from playlist
if args.remove_from_playlist:
remove_video_from_playlist(youtube, plitem["id"], playlist_id, args.dry_run)
return 0 return 0

View File

@ -2,3 +2,5 @@ google-api-python-client
google-auth google-auth
google-auth-httplib2 google-auth-httplib2
google-auth-oauthlib google-auth-oauthlib
tinydb
yt_dlp