aboutsummaryrefslogtreecommitdiff
path: root/yt.py
diff options
context:
space:
mode:
Diffstat (limited to 'yt.py')
-rw-r--r--yt.py146
1 files changed, 146 insertions, 0 deletions
diff --git a/yt.py b/yt.py
new file mode 100644
index 0000000..d819e9c
--- /dev/null
+++ b/yt.py
@@ -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)