diff options
Diffstat (limited to 'yt.py')
-rw-r--r-- | yt.py | 146 |
1 files changed, 146 insertions, 0 deletions
@@ -0,0 +1,146 @@ +# +# YouTube video info command +# +# Copyright © 2023 by luk3yx +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# + +import math, requests, random, time, traceback + + +# Default values so that the command still works if api.invidious.io is down +cache_expiry = -math.inf +instances = ['yewtu.be'] +api_instances = ['i.psf.lt'] + + +def fetch_instances(irc, args): + global cache_expiry, instances, api_instances + + # Refresh the cache at most once per day + if time.monotonic() >= cache_expiry or not instances or not api_instances: + try: + resp = requests.get('https://api.invidious.io/instances.json') + resp.raise_for_status() + + new_instances = [] + new_api_instances = [] + for _, info in resp.json(): + if (info['type'] != 'https' or not info.get('stats') or + not info['stats'].get('openRegistrations')): + continue + + new_instances.append(info['uri']) + if info['api']: + new_api_instances.append(info['uri']) + + if new_instances and new_api_instances: + cache_expiry = time.monotonic() + 86400 + instances = new_instances + api_instances = new_api_instances + except (requests.RequestException, IndexError, ValueError) as exc: + # This is okay, just use the cache for now + traceback.print_exc() + irc.notice(args[0], f'{exc.__class__.__name__} when fetching ' + f'Invidious instance list, using cache.') + + return instances, api_instances + + +RED = '\x0304' +GREEN = '\x0303' + + +def get_url_and_metadata(irc, args, video_id): + instances, api_instances = fetch_instances(irc, args) + api_url = f'{random.choice(api_instances)}' + res = requests.get(f'{api_url}/api/v1/videos/{video_id}', params={ + 'fields': 'title,liveNow,lengthSeconds,author,publishedText,error' + }) + return f'{random.choice(instances)}/watch?v={video_id}', res.json() + + +def format_length(seconds): + minutes = seconds // 60 + seconds %= 60 + if minutes < 60: + return f'{minutes}:{seconds:02}' + return f'{minutes // 60}:{minutes % 60:02}:{seconds % 60:02}' + + +def num(n): + precision = 0 + for prefix in ('', ' K', ' M'): + if n < 1000: + return f'\x02{n:.{precision}f}{prefix}\x02' + n /= 1000 + precision = 1 + return f'\x02{n:.{precision}f} B\x02' + + +@register_command('yt', 'youtube') +def youtube(irc, hostmask, is_admin, args): + video_url = args[1] + if 'v=' in video_url: + video_id = video_url.split('v=', 1)[1] + elif '/shorts/' in video_url: + video_id = video_url.split('/shorts/', 1)[1] + elif video_url.startswith(('youtu.be/', 'https://youtu.be/', + 'http://youtu.be/')): + video_id = video_url.split('youtu.be/', 1)[1] + else: + irc.msg(args[0], 'Usage: .yt <video URL>') + return + video_id = video_id.split('?', 1)[0].split('&', 1)[0].split('#', 1)[0] + res = requests.get('https://returnyoutubedislikeapi.com/votes', + params={'videoId': video_id}) + if res.status_code in (400, 404): + irc.msg(args[0], 'Invalid video ID!') + return + res.raise_for_status() + data = res.json() + + url, md = get_url_and_metadata(irc, args, video_id) + ok = not md or 'error' not in md + + if video_id == 'dQw4w9WgXcQ': + md['title'] = 'Normal video' + md['author'] = 'Definitely not Rick Astley' + + parts = [] + if ok: + parts.append(md['title']) + if md['liveNow']: + parts.append(f'{RED}\x02• Live now\x02') + else: + parts.append(format_length(md['lengthSeconds'])) + parts.append(f'Channel: {md["author"]}') + else: + error_msg = md.get("error", "Unknown error") + if error_msg == "Sign in if you've been granted access to this video": + error_msg = 'The video has been made private' + parts.append(f'Could not fetch video metadata: {error_msg}') + parts.append(f'Views: {num(data["viewCount"])}') + if ok and not md['liveNow']: + parts.append(f'Published {md["publishedText"]}') + parts.append(f'Likes: {GREEN}{num(data["likes"])}') + parts.append(f'Dislikes: {RED}{num(data["dislikes"])}\x03 ' + f'(from returnyoutubedislike.com)') + if ok: + parts.append(f'Link: {url}') + + msg = ' | '.join(part + '\x03' if '\x03' in part else part + for part in parts) + irc.msg(args[0], hostmask[0] + ': ' + msg) |