Skip to content

Commit

Permalink
Use yt-dlp to fetch video information for queue items (#3)
Browse files Browse the repository at this point in the history
Add a new VideoQueue class that manages a queue of video info that is
mirrored to the MPV queue except with additional information about each
video. This can be used to show richer queue information but requires
more work before it can be used to directly give video URLs to MPV as
most streams will be broken into multiple streams (audio+video) and
loadfile only supports a single URL.
  • Loading branch information
Douile committed Aug 20, 2023
1 parent 06399e1 commit f019bf9
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 28 deletions.
2 changes: 1 addition & 1 deletion format.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh

black ./friends_queue/
black ./friends_queue/ ./main.py
prettier -w ./friends_queue/
2 changes: 2 additions & 0 deletions friends_queue/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import video_queue
from . import friends_queue
3 changes: 0 additions & 3 deletions friends_queue/__main__.py

This file was deleted.

66 changes: 42 additions & 24 deletions friends_queue/friends_queue.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3

import mpv
import yt_dlp

import threading
import sys
Expand All @@ -10,6 +11,9 @@
from math import floor
import os

from .video_queue import VideoQueue


# Address to listen on
ADDRESS = ("0.0.0.0", 8000)
# Specify which format to select https://github.com/yt-dlp/yt-dlp#format-selection
Expand Down Expand Up @@ -47,7 +51,7 @@ def seconds_duration(secs: float):
return "{:02}:{:02}:{:02}".format(floor(hours), floor(mins), floor(secs))


def http_handler(player: mpv.MPV):
def http_handler(player: mpv.MPV, queue: VideoQueue):
"""Create a HTTPHandler class with encapsulated player"""

class HTTPHandler(BaseHTTPRequestHandler):
Expand All @@ -66,10 +70,8 @@ def do_GET(self):
extra = "?"
if "link" in opts:
redir = True
print("Handle this", opts["link"])
player.playlist_append(opts["link"])
if player.playlist_pos < 0:
player.playlist_pos = player.playlist_count - 1
print("Adding to queue", opts["link"])
queue.append_url(opts["link"])
if "a" in opts:
redir = True
a = opts["a"]
Expand Down Expand Up @@ -131,7 +133,7 @@ def do_GET(self):
)
self.wfile.write(STYLE)
self.wfile.write(b"</style></head><body>")
generate_page(self.wfile, player, text)
generate_page(self.wfile, player, queue, text)
self.wfile.write(b"</body>")

return HTTPHandler
Expand All @@ -143,7 +145,7 @@ def generate_action_button(wfile, action: str):
)


def generate_page(wfile, player, text):
def generate_page(wfile, player: mpv.MPV, queue: VideoQueue, text: str):
"""Generate the body of a page"""
if len(text) > 0:
wfile.write(
Expand Down Expand Up @@ -190,52 +192,68 @@ def generate_page(wfile, player, text):
"utf-8",
)
)
# Volume
if player.volume is not None:
wfile.write(b'<form class="grid volume">')
generate_action_button(wfile, "volume_down")
wfile.write(bytes("<span>{:.0f}</span>".format(player.volume), "utf-8"))
generate_action_button(wfile, "volume_up")
wfile.write(b"</form>")
# Volume
if player.volume is not None:
wfile.write(b'<form class="grid volume">')
generate_action_button(wfile, "volume_down")
wfile.write(bytes("<span>{:.0f}</span>".format(player.volume), "utf-8"))
generate_action_button(wfile, "volume_up")
wfile.write(b"</form>")
# Playlist
player_current = player.playlist_pos

wfile.write(b"<ol>")
for item in player.playlist:
for i in range(0, len(queue)):
item = queue[i]
current = i == player_current

content = "<li>"
if item.get("current"):
if current:
content += "<strong>"
if "title" in item:
if item.title is not None:
content += html.escape(
"{} ({})".format(item.get("title"), item.get("filename"))
"{} - {} ({}) {}".format(
item.uploader, item.title, item.url, item.duration_str
)
)
else:
content += html.escape(item.get("filename"))
if item.get("current"):
content += html.escape(item.url)
if current:
content += "</strong>"
content += "</li>"
wfile.write(bytes(content, "utf-8"))
wfile.write(b"</ol>")


def main():
def main(debug=False):
extra_args = {}
if debug:
extra_args["log_handler"] = print
extra_args["loglevel"] = "debug"
player = mpv.MPV(
ytdl=True,
ytdl_format=FORMAT_SPECIFIER,
input_default_bindings=True,
input_vo_keyboard=True,
osc=True,
idle=True,
log_handler=print,
loglevel="debug",
**extra_args,
)

http = HTTPThread(ADDRESS, http_handler(player))
ytdl = yt_dlp.YoutubeDL({"format": FORMAT_SPECIFIER})

queue = VideoQueue(player, ytdl)

http = HTTPThread(ADDRESS, http_handler(player, queue))
http.start()

try:
player.wait_for_shutdown()
except Exception as e:
print(e)

del player

print("Shutting down")
http.shutdown()

Expand Down
76 changes: 76 additions & 0 deletions friends_queue/video_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import mpv
import yt_dlp

from typing import List
from dataclasses import dataclass
from threading import Thread


@dataclass
class VideoQueueItem:
url: str
title: str = None
uploader: str = None
duration: int = None
duration_str: str = None
stream_url: str = None
thumbnail: str = None


# TODO: Lock queue
# TODO: Async ytdl fetch
class VideoQueue(List[VideoQueueItem]):
def __init__(self, player: mpv.MPV, ytdl: yt_dlp.YoutubeDL):
super().__init__()
self._player = player
self._ytdl = ytdl

def append(self, item: VideoQueueItem):
assert item is not None
super().append(item)
self._player.loadfile(item.url, mode="append-play")

def append_url(self, url: str):
self.append(fetch_video(self._ytdl, url))

def move(self, item: int, to: int):
assert item >= 0 and item < len(self)
assert to >= 0 and to < len(self)

if item == to:
return None

item_value = self[item]
if item < to:
for i in range(item, to):
self[i] = self[i + 1]
else:
for i in range(to + 1, item):
self[i] = self[i - 1]
self[to] = item_value

self._player.playlist_move(item, to)


class FetchVideoThread(Thread):
def __init__(self, ytdl: yt_dlp.YoutubeDL, item: VideoQueueItem):
super().__init__()
self._ytdl = ytdl
self._item = item

def run(self):
info = self._ytdl.extract_info(self._item.url, download=False)
self._item.title = info.get("fulltitle")
self._item.uploader = info.get("uploader")
self._item.stream_url = None # TODO
self._item.duration = info.get("duration")
self._item.duration_str = info.get("duration_string")
self._item.thumbnail = info.get("thumbnail") # TODO: Select thumbnail res


def fetch_video(ytdl: yt_dlp.YoutubeDL, url: str) -> VideoQueueItem:
item = VideoQueueItem(url)

FetchVideoThread(ytdl, item).start()

return item
5 changes: 5 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env python3

from friends_queue.friends_queue import main

main()

0 comments on commit f019bf9

Please sign in to comment.