diff --git a/.gitignore b/.gitignore index e80c616..0826348 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ tags .env env/ .idea/ -venv/ \ No newline at end of file +venv/ +test.py \ No newline at end of file diff --git a/VERSION b/VERSION index 5da122e..a14b53f 100644 --- a/VERSION +++ b/VERSION @@ -1,3 +1,3 @@ # This file is used by clients to check for updates -version 1.1.1 \ No newline at end of file +version 1.1.2 \ No newline at end of file diff --git a/mps_youtube/__init__.py b/mps_youtube/__init__.py index bfb988c..b73d4b4 100644 --- a/mps_youtube/__init__.py +++ b/mps_youtube/__init__.py @@ -1,5 +1,5 @@ __version__ = "1.1.1" -__notes__ = "released on 28 Jan 2022" +__notes__ = "released on 06 Feb 2022" __author__ = "iamtalhaasghar" __license__ = "GPLv3" __url__ = "https://github.com/iamtalhaasghar/yewtube" diff --git a/mps_youtube/commands/album_search.py b/mps_youtube/commands/album_search.py index 9c500c9..a09045e 100644 --- a/mps_youtube/commands/album_search.py +++ b/mps_youtube/commands/album_search.py @@ -10,7 +10,7 @@ from xml.etree import ElementTree as ET from .. import c, g, screen, __version__, __url__, content, config, util from . import command from .songlist import paginatesongs -from .search import generate_search_qs, get_tracks_from_json +from .search import get_tracks_from_json def show_message(message, col=c.r, update=False): @@ -120,7 +120,7 @@ def _match_tracks(artist, title, mb_tracks): dtime(length))) q = "%s %s" % (artist, ttitle) w = q = ttitle if artist == "Various Artists" else q - query = generate_search_qs(w, 0) + query = w#generate_search_qs(w, 0) util.dbg(query) # perform fetch diff --git a/mps_youtube/commands/search.py b/mps_youtube/commands/search.py index 934004b..621e581 100644 --- a/mps_youtube/commands/search.py +++ b/mps_youtube/commands/search.py @@ -29,12 +29,12 @@ DAYS = dict(day = 1, year = 365) -def _search(progtext, qs=None, msg=None, failmsg=None): +def _search(progtext, query, msg=None, failmsg=None): """ Perform memoized url fetch, display progtext. """ loadmsg = "Searching for '%s%s%s'" % (c.y, progtext, c.w) - wdata = pafy.video_search(qs['q']) + wdata = pafy.video_search(query) def iter_songs(): wdata2 = wdata while True: @@ -43,7 +43,7 @@ def _search(progtext, qs=None, msg=None, failmsg=None): if type(wdata2) is list or not wdata2.get('nextPageToken'): break - qs['pageToken'] = None#wdata2['nextPageToken'] + query = None#wdata2['nextPageToken'] wdata2 = None#pafy.call_gdata('search', qs) # The youtube search api returns a maximum of 500 results @@ -67,40 +67,40 @@ def token(page): return b64.strip('=') -def generate_search_qs(term, match='term', videoDuration='any', after=None, category=None, is_live=False): - """ Return query string. """ - - aliases = dict(views='viewCount') - qs = { - 'q': term, - 'maxResults': 50, - 'safeSearch': "none", - 'order': aliases.get(config.ORDER.get, config.ORDER.get), - 'part': 'id,snippet', - 'type': 'video', - 'videoDuration': videoDuration - #,'key': config.API_KEY.get - } - - if after: - after = after.lower() - qs['publishedAfter'] = '%sZ' % (datetime.utcnow() - timedelta(days=DAYS[after])).isoformat() \ - if after in DAYS.keys() else '%s%s' % (after, 'T00:00:00Z' * (len(after) == 10)) - - if match == 'related': - qs['relatedToVideoId'] = term - del qs['q'] - - if config.SEARCH_MUSIC.get: - qs['videoCategoryId'] = 10 - - if category: - qs['videoCategoryId'] = category - - if is_live: - qs['eventType'] = "live" - - return qs +# def generate_search_qs(term, match='term', videoDuration='any', after=None, category=None, is_live=False): +# """ Return query string. """ +# +# aliases = dict(views='viewCount') +# qs = { +# 'q': term, +# 'maxResults': 50, +# 'safeSearch': "none", +# 'order': aliases.get(config.ORDER.get, config.ORDER.get), +# 'part': 'id,snippet', +# 'type': 'video', +# 'videoDuration': videoDuration +# #,'key': config.API_KEY.get +# } +# +# if after: +# after = after.lower() +# qs['publishedAfter'] = '%sZ' % (datetime.utcnow() - timedelta(days=DAYS[after])).isoformat() \ +# if after in DAYS.keys() else '%s%s' % (after, 'T00:00:00Z' * (len(after) == 10)) +# +# if match == 'related': +# qs['relatedToVideoId'] = term +# del qs['q'] +# +# if config.SEARCH_MUSIC.get: +# qs['videoCategoryId'] = 10 +# +# if category: +# qs['videoCategoryId'] = category +# +# if is_live: +# qs['eventType'] = "live" +# +# return qs def userdata_cached(userterm): @@ -208,7 +208,7 @@ def usersearch_id(user, channel_id, term): for an optional search term with the user (i.e. the channel) identified by its ID """ - query = generate_search_qs(term) + query = None#generate_search_qs(term) aliases = dict(views='viewCount') # The value of the config item is 'views' not 'viewCount' if config.USER_ORDER.get: query['order'] = aliases.get(config.USER_ORDER.get, @@ -230,22 +230,21 @@ Use 'set search_music False' to show results not in the Music category.""" % ter failmsg = "User %s not found or has no videos." % termuser[1] msg = str(msg).format(c.w, c.y, c.y, term, user) - _search(progtext, query, msg, failmsg) + _search(progtext, term, msg, failmsg) def related_search(vitem): - """ Fetch uploads by a YouTube user. """ - query = generate_search_qs(vitem.ytid, match='related') - - if query.get('videoCategoryId'): - del query['videoCategoryId'] + """ Fetch videos related to vitem + vitem = {'description': str, 'length': int, 'title': str, 'ytid': str} + """ t = vitem.title ttitle = t[:48].strip() + ".." if len(t) > 49 else t msg = "Videos related to %s%s%s" % (c.y, ttitle, c.w) failmsg = "Related to %s%s%s not found" % (c.y, vitem.ytid, c.w) - _search(ttitle, query, msg, failmsg) + + _search(ttitle, vitem.title, msg, failmsg) # Livestream category search @@ -309,12 +308,11 @@ def search(term): return logging.info("search for %s", term) - query = generate_search_qs(term, videoDuration=video_duration, after=after, - category=args.category, is_live=args.live) + query = None#generate_search_qs(term, videoDuration=video_duration, after=after, category=args.category, is_live=args.live) msg = "Search results for %s%s%s" % (c.y, term, c.w) failmsg = "Found nothing for %s%s%s" % (c.y, term, c.w) - _search(term, query, msg, failmsg) + _search(term, term, msg, failmsg) @command(r'u(?:ser)?pl\s(.*)', 'userpl', 'upl') @@ -350,35 +348,31 @@ def pl_search(term, page=0, splash=True, is_user=False): else: # playlist search is done with the above url and param type=playlist logging.info("playlist search for %s", prog) - qs = generate_search_qs(term) - qs['pageToken'] = token(page) - qs['type'] = 'playlist' - if 'videoCategoryId' in qs: - del qs['videoCategoryId'] # Incompatable with type=playlist + # qs = generate_search_qs(term) + # qs['pageToken'] = token(page) + # qs['type'] = 'playlist' + # if 'videoCategoryId' in qs: + # del qs['videoCategoryId'] # Incompatable with type=playlist - pldata = None#pafy.call_gdata('search', qs) + pldata = pafy.playlist_search(term) - id_list = [i.get('id', {}).get('playlistId') - for i in pldata.get('items', ()) - if i['id']['kind'] == 'youtube#playlist'] + #id_list = [i.get('id', {}) for i in pldata] - result_count = min(pldata['pageInfo']['totalResults'], 500) - - qs = {'part': 'contentDetails,snippet', - 'maxResults': 50} + result_count = len(pldata) + #todo: what is the purpose of this code? #qs = {'part': 'contentDetails,snippet','maxResults': 50} if is_user: if page: - qs['pageToken'] = token(page) - qs['channelId'] = channel_id + pass #qs['pageToken'] = token(page) + pass #qs['channelId'] = channel_id else: - qs['id'] = ','.join(id_list) + pass #qs['id'] = ','.join(id_list) - pldata = None#pafy.call_gdata('playlists', qs) + pldata = pafy.playlist_search(term) playlists = get_pl_from_json(pldata)[:util.getxy().max_results] - if is_user: - result_count = pldata['pageInfo']['totalResults'] + # if is_user: + # result_count = pldata['pageInfo']['totalResults'] if playlists: g.last_search_query = (pl_search, {"term": term, "is_user": is_user}) @@ -399,7 +393,7 @@ def get_pl_from_json(pldata): """ Process json playlist data. """ try: - items = pldata['items'] + items = pldata except KeyError: items = [] @@ -407,15 +401,14 @@ def get_pl_from_json(pldata): results = [] for item in items: - snippet = item['snippet'] results.append(dict( link=item["id"], - size=item["contentDetails"]["itemCount"], - title=snippet["title"], - author=snippet["channelTitle"], - created=snippet["publishedAt"], - updated=snippet['publishedAt'], #XXX Not available in API? - description=snippet["description"])) + size=item["videoCount"], + title=item["title"], + author=item['channel']["name"], + created=item.get("publishedAt"), + updated=item.get('publishedAt'), #XXX Not available in API? + description=item.get("description"))) return results @@ -448,17 +441,7 @@ def get_tracks_from_json(jsons): for item in jsons: try: ytid = get_track_id_from_json(item) - duration = item.get('duration') - - if duration: - duration_tokens = duration.split(":") - if len(duration_tokens) == 2: - duration = int(duration_tokens[0]) * 60 + int(duration_tokens[1]) - else: - duration = int(duration_tokens[0]) * 3600 + int(duration_tokens[1]) * 60 + int(duration_tokens[2]) - else: - duration = 30 - + duration = util.parse_video_length(item.get('duration')) stats = item.get('statistics', {}) snippet = item.get('snippet', {}) title = item.get('title', '').strip() diff --git a/mps_youtube/commands/songlist.py b/mps_youtube/commands/songlist.py index 986f842..91cc222 100644 --- a/mps_youtube/commands/songlist.py +++ b/mps_youtube/commands/songlist.py @@ -2,7 +2,7 @@ import math import random -from .. import g, c, screen, streams, content, util +from .. import g, c, screen, streams, content, util, pafy from ..playlist import Video from . import command, PL @@ -83,16 +83,16 @@ def plist(parturl): ytpl, plitems = g.pafy_pls[parturl] else: util.dbg("%sFetching playlist using pafy%s", c.y, c.w) - ytpl = None#pafy.get_playlist2(parturl) - plitems = util.IterSlicer(ytpl) + ytpl = pafy.get_playlist(parturl) + plitems = util.IterSlicer(ytpl['videos']) g.pafy_pls[parturl] = (ytpl, plitems) def pl_seg(s, e): - return [Video(i.videoid, i.title, i.length) for i in plitems[s:e]] + return [Video(i['id'], i['title'], util.parse_video_length(i['duration'])) for i in plitems[s:e]] - msg = "Showing YouTube playlist %s" % (c.y + ytpl.title + c.w) + msg = "Showing YouTube playlist %s" % (c.y + ytpl['info']['title'] + c.w) loadmsg = "Retrieving YouTube playlist" - paginatesongs(pl_seg, length=len(ytpl), msg=msg, loadmsg=loadmsg) + paginatesongs(pl_seg, length=len(ytpl['videos']), msg=msg, loadmsg=loadmsg) @command(r'(rm|add)\s*(-?\d[-,\d\s]{,250})', 'rm', 'add') diff --git a/mps_youtube/commands/spotify_playlist.py b/mps_youtube/commands/spotify_playlist.py index 1d4ab9f..5952dac 100644 --- a/mps_youtube/commands/spotify_playlist.py +++ b/mps_youtube/commands/spotify_playlist.py @@ -15,7 +15,7 @@ except ImportError: from .. import c, g, screen, __version__, __url__, content, config, util from . import command from .songlist import paginatesongs -from .search import generate_search_qs, get_tracks_from_json +from .search import get_tracks_from_json def generate_credentials(): @@ -136,7 +136,7 @@ def _match_tracks(tracks): dtime(length))) q = "%s %s" % (artist, ttitle) w = q = ttitle if artist == "Various Artists" else q - query = generate_search_qs(w, 0) + query = w#generate_search_qs(w, 0) util.dbg(query) # perform fetch diff --git a/mps_youtube/init.py b/mps_youtube/init.py index d08b121..99cf451 100644 --- a/mps_youtube/init.py +++ b/mps_youtube/init.py @@ -264,14 +264,10 @@ def _get_version_info(): # pafy_version += " (" + pafy.backend + " backend)" # if pafy.backend == "youtube-dl": - import youtube_dl - youtube_dl_version = youtube_dl.version.__version__ + from yt_dlp.version import __version__ as ytdlp_version - out = "yewtube version : " + __version__ - out += "\n notes : " + __notes__ - #out += "\npafy version : " + pafy_version - if youtube_dl_version: - out += "\nyoutube-dl version : " + youtube_dl_version + out = "yewtube version : " + __version__ +" "+ __notes__ + out += "\nyt_dlp version : " + ytdlp_version out += "\nPython version : " + sys.version out += "\nProcessor : " + platform.processor() out += "\nMachine type : " + platform.machine() diff --git a/mps_youtube/pafy.py b/mps_youtube/pafy.py index 33e5e66..0f14e29 100644 --- a/mps_youtube/pafy.py +++ b/mps_youtube/pafy.py @@ -1,5 +1,5 @@ -from youtubesearchpython import VideosSearch -import yt_dlp +from youtubesearchpython import VideosSearch, ChannelsSearch, PlaylistsSearch, Suggestions, Playlist +import yt_dlp, random class MyLogger: def debug(self, msg): @@ -24,7 +24,24 @@ def get_video_streams(ytid): with yt_dlp.YoutubeDL({'logger':MyLogger()}) as ydl: info_dict = ydl.extract_info(ytid, download=False) return [i for i in info_dict['formats'] if i.get('format_note') != 'storyboard'] + def video_search(query): videosSearch = VideosSearch(query, limit=50) wdata = videosSearch.result()['result'] - return wdata \ No newline at end of file + return wdata + +def channel_search(query): + channelsSearch = ChannelsSearch(query, limit=50, region='US') + return channelsSearch.result()['result'] + +def playlist_search(query): + playlistsSearch = PlaylistsSearch(query, limit=50) + return playlistsSearch.result()['result'] + +def get_playlist(playlist_id): + playlist = Playlist.get('https://www.youtube.com/playlist?list=%s' % playlist_id) + return playlist +def get_video_title_suggestions(query): + suggestions = Suggestions(language = 'en', region = 'US') + related_searches = suggestions.get(query)['result'] + return related_searches[random.randint(0,len(related_searches))] \ No newline at end of file diff --git a/mps_youtube/util.py b/mps_youtube/util.py index 1e494a9..1f7cc28 100644 --- a/mps_youtube/util.py +++ b/mps_youtube/util.py @@ -342,6 +342,8 @@ def real_len(u, alt=False): def yt_datetime(yt_date_time): """ Return a time object, locale formated date string and locale formatted time string. """ + if yt_date_time is None: + return ['Unknown', 'Unknown', 'Unknown'] time_obj = time.strptime(yt_date_time, "%Y-%m-%dT%H:%M:%SZ") locale_date = time.strftime("%x", time_obj) locale_time = time.strftime("%X", time_obj) @@ -593,3 +595,16 @@ class CommandCompleter: def add_cmd(self, val): if(not val in self.COMMANDS): self.COMMANDS.append(val) + +def parse_video_length(duration): + ''' + Converts HH:MM:SS to a single integer .i.e. total number of seconds + ''' + if duration: + duration_tokens = duration.split(":") + if len(duration_tokens) == 2: + return int(duration_tokens[0]) * 60 + int(duration_tokens[1]) + else: + return int(duration_tokens[0]) * 3600 + int(duration_tokens[1]) * 60 + int(duration_tokens[2]) + else: + return 10 diff --git a/requirements.txt b/requirements.txt index a20051f..0fce393 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ pyreadline yt-dlp -youtube-search-python -youtube_dl \ No newline at end of file +youtube-search-python \ No newline at end of file