Add download option
This commit is contained in:
parent
080842758d
commit
a352f3cac0
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
96
main.py
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user