anki-search-inside-add-card icon indicating copy to clipboard operation
anki-search-inside-add-card copied to clipboard

Batch Load Youtube videos from a playlist link

Open ghost opened this issue 5 years ago • 5 comments
trafficstars

It would be really awesome to batch load MIT's OCW courses -as well as my university courses as they shift towards e-learning thanks to the pandemic- and incrementally watch them since I use them as supplement material to my university's courses. I assume others would find it particularly appealing too due to the now wide availability of free Youtube courses packed in playlist formats.

ghost avatar Nov 08 '20 22:11 ghost

I'm not fonol, but I wrote this new YouTube quick import thing, which automatically copies the youtube title and channel name into the new note. If fonol approves of this feature, I can have a look at the feasibility of doing this.

p4nix avatar Nov 09 '20 06:11 p4nix

@p4nix Sure, sounds like a useful feature!

fonol avatar Nov 09 '20 06:11 fonol

so i managed to do it over the last couple days - just to test the concept - and it worked (similar to how the url dialogue works), however i used youtube-dl library to parse the playlist's videos' ids, so i am not sure you would want to integrate that (because of DMCA issues).

from aqt.qt import *
import aqt.editor
import aqt
import functools
import re
from ..notes import *
import random
from aqt.utils import showInfo
import youtube_dl
from ..index.fts_index import Worker
import utility.text
import utility.misc
from ..youtube_dl import YoutubeDL
import utility.misc
YOUTUBE_URL_REGEX = re.compile(r"^(http|https):\/\/(?:www\.)?youtube\.com\/(?:watch|playlist)\?(?:&.*)*((?:v=([^&\s]*)(?:&.*)*&list=(?P<ListID>[^&\s]*)|(?:list=(?P<ListID1>[^&\s])*)(?:&.*)*&v=([^&\s]*)|(?:list=(?P<ListID2>[^#\&\?]*))))(?:&.*)*(?:\#.*)*")

class YoutubePlayListDialog(QDialog):
	"""Fetches the url"""
	
	def __init__(self, parent):
		QDialog.__init__(self, parent, Qt.WindowSystemMenuHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
		self.chosen_url = None
		self.parent = parent
		self.videos = None
		self.setup_ui()
		self.setWindowTitle("Youtube Playlist Input")
		
		self.threadPool = QThreadPool()
		
	
	def setup_ui(self):
		self.vbox = QVBoxLayout()
		self.vbox.addWidget(QLabel("This will import the Playlist videos."))
		self.vbox.addSpacing(10)
		self.vbox.addWidget(QLabel("URL:"))
		self.input = QLineEdit()
		self.input.setMinimumWidth(300)
		self.vbox.addWidget(self.input)
		self.vbox.addSpacing(15)
		
		hbox_bot = QHBoxLayout()
		self.accept_btn = QPushButton("Fetch")
		self.accept_btn.setShortcut("Ctrl+Return")
		self.accept_btn.clicked.connect(self.accept_clicked)
		
		self.reject_btn = QPushButton("Cancel")
		self.reject_btn.clicked.connect(self.reject)
		hbox_bot.addStretch(1)
		hbox_bot.addWidget(self.accept_btn)
		hbox_bot.addWidget(self.reject_btn)
		self.vbox.addLayout(hbox_bot)
		
		self.setLayout(self.vbox)
	
	def _is_youtube_playlist(self, url: str) -> bool:
		return url is not None and (YOUTUBE_URL_REGEX.match(url).group("ListID")  or  \
		                            YOUTUBE_URL_REGEX.match(url).group("ListID1") or  \
		                            YOUTUBE_URL_REGEX.match(url).group("ListID2")) is not None
		
	def accept_clicked(self):
		self.chosen_url = self.input.text()
		if self._is_youtube_playlist(self.chosen_url):
			worker = Worker(self._load_yt_playlistvids,self.chosen_url)
			worker.signals = WorkerSignals()
			worker.signals.result.connect(self._success)
			worker.stamp    = utility.misc.get_milisec_stamp()
			self.threadPool.start(worker)
		
	def _success(self, vids: tuple,nothing):
		self.videos = vids
		self.accept()
	
	def _load_yt_playlistvids(self, playlist: str):
		ydl_opts = {
					 'ignoreerrors'  : True,
					 'quiet'         : True,
					 'yesplaylist'   : True,
					}
		result = ()
		with YoutubeDL(ydl_opts) as ydl:
			playlist_dict = ydl.extract_info(playlist, download=False)
			for video in playlist_dict['entries']:
				video_details = {}
				if not video:
					# TODO add to logger
					print('ERROR: Unable to get info. Continuing...')
					continue
				video_details["source"] = f'https://www.youtube.com/watch?v={video.get("id")}'
				video_details["title"] = video.get("title")
				result = result + (video_details,)
		return result


class WorkerSignals(QObject):
    finished    = pyqtSignal()
    error       = pyqtSignal(tuple)
    result      = pyqtSignal(tuple,object)

ghost avatar Nov 12 '20 04:11 ghost

Thanks for the code. To be honest, I would prefer a solution without that external library. This add-on is already quite bloated, so I don't really like including just another library just for an edge-usecase. I could imagine using YouTube's data API, which has the advantage of being 100% safe and legal and would eliminate the need to include any additional libraries. However, you need an API key, and I am also not really a fan of solutions that require external setup of any kind to be usable.

fonol avatar Nov 12 '20 18:11 fonol

Perhaps going with a user-configurable YouTube API key would be the way? - and would also allow to extend the Quick YT dialog with a search function, as I envisioned in the beginning. But this would add to the complexity again... And there is so much on the list next to studying already anyway ^^

p4nix avatar Nov 12 '20 20:11 p4nix