fonol / anki-search-inside-add-card

An add-on providing full-text-search and PDF reading functionality to Anki's Add card dialog
https://ankiweb.net/shared/info/1781298089
GNU Affero General Public License v3.0
179 stars 24 forks source link

Batch Load Youtube videos from a playlist link #157

Open ghost opened 4 years ago

ghost commented 4 years ago

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.

p4nix commented 4 years ago

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.

fonol commented 4 years ago

@p4nix Sure, sounds like a useful feature!

ghost commented 4 years ago

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)
fonol commented 4 years ago

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.

p4nix commented 4 years ago

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 ^^