From 988832acfb2bdf466d894fb24691ae32eb4319e5 Mon Sep 17 00:00:00 2001
From: BarsTiger Songs in playlist
Advanced search
")) + self.searchButton.setText(_translate("MainWindow", "Search")) + self.pushButton.setText(_translate("MainWindow", "Add selected")) diff --git a/horsydist/resources/gui/Ui_RPCsettings.py b/horsydist/resources/gui/Ui_RPCsettings.py new file mode 100644 index 0000000..ee3dd4e --- /dev/null +++ b/horsydist/resources/gui/Ui_RPCsettings.py @@ -0,0 +1,39 @@ +from PyQt5 import QtCore, QtWidgets +from resources.lib.config import config + + +class Ui_RPCsettings(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(160, 100) + MainWindow.setMinimumSize(QtCore.QSize(160, 100)) + MainWindow.setMaximumSize(QtCore.QSize(160, 100)) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.verticalLayoutWidget = QtWidgets.QWidget(self.centralwidget) + self.verticalLayoutWidget.setGeometry(QtCore.QRect(9, 0, 151, 80)) + self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.ShowRPCcheckBox = QtWidgets.QCheckBox(self.verticalLayoutWidget) + self.ShowRPCcheckBox.setObjectName("ShowRPCcheckBox") + self.verticalLayout.addWidget(self.ShowRPCcheckBox) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 160, 21)) + self.menubar.setObjectName("menubar") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.ShowRPCcheckBox.setChecked(config['showrpc']) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "RPC settings")) + self.ShowRPCcheckBox.setText(_translate("MainWindow", "Show RPC")) \ No newline at end of file diff --git a/horsydist/resources/gui/Ui_Settings.py b/horsydist/resources/gui/Ui_Settings.py new file mode 100644 index 0000000..d807676 --- /dev/null +++ b/horsydist/resources/gui/Ui_Settings.py @@ -0,0 +1,48 @@ +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Settings(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(160, 100) + MainWindow.setMinimumSize(QtCore.QSize(160, 100)) + MainWindow.setMaximumSize(QtCore.QSize(160, 100)) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.verticalLayoutWidget = QtWidgets.QWidget(self.centralwidget) + self.verticalLayoutWidget.setGeometry(QtCore.QRect(0, 0, 160, 80)) + self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + + self.updateButton = QtWidgets.QPushButton(self.verticalLayoutWidget) + self.updateButton.setObjectName("updateButton") + self.verticalLayout.addWidget(self.updateButton) + + self.appBuildButton = QtWidgets.QPushButton(self.verticalLayoutWidget) + self.appBuildButton.setObjectName("appBuildButton") + self.verticalLayout.addWidget(self.appBuildButton) + + self.RPCButton = QtWidgets.QPushButton(self.verticalLayoutWidget) + self.RPCButton.setObjectName("RPCButton") + self.verticalLayout.addWidget(self.RPCButton) + + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 160, 21)) + self.menubar.setObjectName("menubar") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "Settings")) + self.updateButton.setText(_translate("MainWindow", "Upgrade player")) + self.appBuildButton.setText(_translate("MainWindow", "Choose main build")) + self.RPCButton.setText(_translate("MainWindow", "Discord RPC settings")) diff --git a/horsydist/resources/gui/Ui_Updater.py b/horsydist/resources/gui/Ui_Updater.py new file mode 100644 index 0000000..5970d6b --- /dev/null +++ b/horsydist/resources/gui/Ui_Updater.py @@ -0,0 +1,37 @@ +from PyQt5 import QtCore, QtWidgets + + +class Ui_Updater(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(160, 97) + MainWindow.setMinimumSize(QtCore.QSize(160, 97)) + MainWindow.setMaximumSize(QtCore.QSize(160, 97)) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.verticalLayoutWidget = QtWidgets.QWidget(self.centralwidget) + self.verticalLayoutWidget.setGeometry(QtCore.QRect(0, 0, 160, 80)) + self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + + self.updateButton = QtWidgets.QPushButton(self.verticalLayoutWidget) + self.updateButton.setObjectName("updateButton") + self.verticalLayout.addWidget(self.updateButton) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 160, 21)) + self.menubar.setObjectName("menubar") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "Updater")) + self.updateButton.setText(_translate("MainWindow", "Download newest")) diff --git a/horsydist/resources/gui/gui.py b/horsydist/resources/gui/gui.py new file mode 100644 index 0000000..a17d5fe --- /dev/null +++ b/horsydist/resources/gui/gui.py @@ -0,0 +1,57 @@ +import sys +from PyQt5 import QtWidgets +from resources.gui.Ui_DelSongs import Ui_DelSongs +from resources.gui.Ui_ExtendedMenu import Ui_ExtendedMenu +from resources.gui.Ui_MainBuild import Ui_MainBuild +from resources.gui.Ui_MainWindow import Ui_MainWindow +from resources.gui.Ui_PlaylistSettings import Ui_PlaylistSettings +from resources.gui.Ui_ProSearch import Ui_ProSearch +from resources.gui.Ui_RPCsettings import Ui_RPCsettings +from resources.gui.Ui_Settings import Ui_Settings +from resources.gui.Ui_Updater import Ui_Updater + + +app = QtWidgets.QApplication(sys.argv) +MainWindow = QtWidgets.QMainWindow() +ui = Ui_MainWindow() +ui.setupUi(MainWindow) + +appPlSet = QtWidgets.QApplication(sys.argv) +MainWindowPlSet = QtWidgets.QMainWindow() +uiPlSet = Ui_PlaylistSettings() +uiPlSet.setupUi(MainWindowPlSet) + +appDelS = QtWidgets.QApplication(sys.argv) +MainWindowDelS = QtWidgets.QMainWindow() +uiDelS = Ui_DelSongs() +uiDelS.setupUi(MainWindowDelS) + +appSet = QtWidgets.QApplication(sys.argv) +MainWindowSet = QtWidgets.QMainWindow() +uiSet = Ui_Settings() +uiSet.setupUi(MainWindowSet) + +appUpd = QtWidgets.QApplication(sys.argv) +MainWindowUpd = QtWidgets.QMainWindow() +uiUpd = Ui_Updater() +uiUpd.setupUi(MainWindowUpd) + +appExt = QtWidgets.QApplication(sys.argv) +MainWindowExt = QtWidgets.QMainWindow() +uiExt = Ui_ExtendedMenu() +uiExt.setupUi(MainWindowExt) + +appPSearch = QtWidgets.QApplication(sys.argv) +MainWindowPSearch = QtWidgets.QMainWindow() +uiPSearch = Ui_ProSearch() +uiPSearch.setupUi(MainWindowPSearch) + +appRPCSet = QtWidgets.QApplication(sys.argv) +MainWindowRPCSet = QtWidgets.QMainWindow() +uiRPCSet = Ui_RPCsettings() +uiRPCSet.setupUi(MainWindowRPCSet) + +appMainBuild = QtWidgets.QApplication(sys.argv) +MainWindowMainBuild = QtWidgets.QMainWindow() +uiMainBuild = Ui_MainBuild() +uiMainBuild.setupUi(MainWindowMainBuild) diff --git a/horsydist/resources/MultiMate.ico b/horsydist/resources/img/MultiMate.ico similarity index 100% rename from horsydist/resources/MultiMate.ico rename to horsydist/resources/img/MultiMate.ico diff --git a/horsydist/resources/MultiMate.png b/horsydist/resources/img/MultiMate.png similarity index 100% rename from horsydist/resources/MultiMate.png rename to horsydist/resources/img/MultiMate.png diff --git a/horsydist/resources/MultiMate40x40.png b/horsydist/resources/img/MultiMate40x40.png similarity index 100% rename from horsydist/resources/MultiMate40x40.png rename to horsydist/resources/img/MultiMate40x40.png diff --git a/horsydist/resources/MultiMate80x80.png b/horsydist/resources/img/MultiMate80x80.png similarity index 100% rename from horsydist/resources/MultiMate80x80.png rename to horsydist/resources/img/MultiMate80x80.png diff --git a/horsydist/resources/hardplaybutton.png b/horsydist/resources/img/hardplaybutton.png similarity index 100% rename from horsydist/resources/hardplaybutton.png rename to horsydist/resources/img/hardplaybutton.png diff --git a/horsydist/resources/hardstopbutton.png b/horsydist/resources/img/hardstopbutton.png similarity index 100% rename from horsydist/resources/hardstopbutton.png rename to horsydist/resources/img/hardstopbutton.png diff --git a/horsydist/resources/next.png b/horsydist/resources/img/next.png similarity index 100% rename from horsydist/resources/next.png rename to horsydist/resources/img/next.png diff --git a/horsydist/resources/prev.png b/horsydist/resources/img/prev.png similarity index 100% rename from horsydist/resources/prev.png rename to horsydist/resources/img/prev.png diff --git a/horsydist/resources/lib/config.py b/horsydist/resources/lib/config.py new file mode 100644 index 0000000..f7dc555 --- /dev/null +++ b/horsydist/resources/lib/config.py @@ -0,0 +1,32 @@ +import os +import json + +configfile = "resources/cfg.cfg" + +if not os.path.isfile(configfile): + cfgwrite = open(configfile, 'w+') + empty = {} + json.dump(empty, cfgwrite, indent=3) + cfgwrite.close() + +with open(configfile) as cfgread: + config = json.load(cfgread) + +try: + config['mainbuild'] +except: + if "MultiMate_Player.py" in os.listdir(os.getcwd()): + config['mainbuild'] = 'MultiMate_Player.py' + elif "MultiMate_Player.pyw" in os.listdir(os.getcwd()): + config['mainbuild'] = 'MultiMate_Player.pyw' + elif "MultiMate_Player.exe" in os.listdir(os.getcwd()): + config['mainbuild'] = 'MultiMate_Player.exe' + with open(configfile, 'w+') as cfgwrite: + json.dump(config, cfgwrite, indent=3) + +try: + config['showrpc'] +except: + config['showrpc'] = True + with open(configfile, 'w+') as cfgwrite: + json.dump(config, cfgwrite, indent=3) diff --git a/horsydist/resources/lib/console.py b/horsydist/resources/lib/console.py new file mode 100644 index 0000000..d2807c7 --- /dev/null +++ b/horsydist/resources/lib/console.py @@ -0,0 +1,5 @@ +import os + + +def cls(): + os.system('cls' if os.name == 'nt' else 'clear') diff --git a/horsydist/resources/lib/rpc.py b/horsydist/resources/lib/rpc.py new file mode 100644 index 0000000..7249975 --- /dev/null +++ b/horsydist/resources/lib/rpc.py @@ -0,0 +1,13 @@ +from pypresence import Presence +from resources.lib.config import config +import time + + +rpc = Presence("896669007342633000") +try: + rpc.connect() + if config['showrpc']: + rpc.update(details="Just started app", state="Nothing is beeing listened...", large_image="multimate", + start=int(time.time())) +except: + pass diff --git a/horsydist/resources/lib/ytsearch.py b/horsydist/resources/lib/ytsearch.py new file mode 100644 index 0000000..ffd72c6 --- /dev/null +++ b/horsydist/resources/lib/ytsearch.py @@ -0,0 +1,65 @@ +import json +import requests +import urllib.parse + + +class YoutubeSearch: + def __init__(self, search_terms: str, max_results=None): + self.search_terms = search_terms + self.max_results = max_results + self.videos = self._search() + + def _search(self): + encoded_search = urllib.parse.quote_plus(self.search_terms) + BASE_URL = "https://youtube.com" + url = f"{BASE_URL}/search?q={encoded_search}" + response = requests.get(url).text + while "ytInitialData" not in response: + response = requests.get(url).text + results = self._parse_html(response) + if self.max_results is not None and len(results) > self.max_results: + return results[: self.max_results] + return results + + def _parse_html(self, response): + results = [] + start = ( + response.index("ytInitialData") + + len("ytInitialData") + + 3 + ) + end = response.index("};", start) + 1 + json_str = response[start:end] + data = json.loads(json_str) + + videos = data["contents"]["twoColumnSearchResultsRenderer"]["primaryContents"][ + "sectionListRenderer" + ]["contents"][0]["itemSectionRenderer"]["contents"] + + for video in videos: + res = {} + if "videoRenderer" in video.keys(): + video_data = video.get("videoRenderer", {}) + res["id"] = video_data.get("videoId", None) + res["thumbnails"] = [thumb.get("url", None) for thumb in video_data.get("thumbnail", {}).get("thumbnails", [{}]) ] + res["title"] = video_data.get("title", {}).get("runs", [[{}]])[0].get("text", None) + res["long_desc"] = video_data.get("descriptionSnippet", {}).get("runs", [{}])[0].get("text", None) + res["channel"] = video_data.get("longBylineText", {}).get("runs", [[{}]])[0].get("text", None) + res["duration"] = video_data.get("lengthText", {}).get("simpleText", 0) + res["views"] = video_data.get("viewCountText", {}).get("simpleText", 0) + res["publish_time"] = video_data.get("publishedTimeText", {}).get("simpleText", 0) + res["url_suffix"] = video_data.get("navigationEndpoint", {}).get("commandMetadata", {}).get("webCommandMetadata", {}).get("url", None) + results.append(res) + return results + + def to_dict(self, clear_cache=True): + result = self.videos + if clear_cache: + self.videos = "" + return result + + def to_json(self, clear_cache=True): + result = json.dumps({"videos": self.videos}) + if clear_cache: + self.videos = "" + return result diff --git a/horsydist/resources/pafy_fix/backend_shared.py b/horsydist/resources/pafy_fix/backend_shared.py new file mode 100644 index 0000000..ecfd4a6 --- /dev/null +++ b/horsydist/resources/pafy_fix/backend_shared.py @@ -0,0 +1,558 @@ +import os +import re +import sys +import time +import logging +import subprocess + +if sys.version_info[:2] >= (3, 0): + # pylint: disable=E0611,F0401,I0011 + from urllib.request import urlopen, build_opener + from urllib.error import HTTPError, URLError + from urllib.parse import parse_qs, urlparse + uni, pyver = str, 3 + +else: + from urllib2 import urlopen, build_opener, HTTPError, URLError + from urlparse import parse_qs, urlparse + uni, pyver = unicode, 2 + +early_py_version = sys.version_info[:2] < (2, 7) + +dbg = logging.debug + + +def extract_video_id(url): + """ Extract the video id from a url, return video id as str. """ + idregx = re.compile(r'[\w-]{11}$') + url = str(url).strip() + + if idregx.match(url): + return url # ID of video + + if '://' not in url: + url = '//' + url + parsedurl = urlparse(url) + if parsedurl.netloc in ('youtube.com', 'www.youtube.com', 'm.youtube.com', 'gaming.youtube.com'): + query = parse_qs(parsedurl.query) + if 'v' in query and idregx.match(query['v'][0]): + return query['v'][0] + elif parsedurl.netloc in ('youtu.be', 'www.youtu.be'): + vidid = parsedurl.path.split('/')[-1] if parsedurl.path else '' + if idregx.match(vidid): + return vidid + + err = "Need 11 character video id or the URL of the video. Got %s" + raise ValueError(err % url) + + +class BasePafy(object): + + """ Class to represent a YouTube video. """ + + def __init__(self, video_url, basic=True, gdata=False, + size=False, callback=None, ydl_opts=None): + """ Set initial values. """ + self.version = 1 + self.videoid = extract_video_id(video_url) + self.watchv_url = "http://www.youtube.com/watch?v=%s" % self.videoid + + self.callback = callback + self._have_basic = False + self._have_gdata = False + + self._description = None + self._likes = None + self._dislikes = None + self._category = None + self._published = None + self._username = None + + self._streams = [] + self._oggstreams = [] + self._m4astreams = [] + self._allstreams = [] + self._videostreams = [] + self._audiostreams = [] + + self._title = None + self._rating = None + self._length = None + self._author = None + self._duration = None + self._keywords = None + self._bigthumb = None + self._viewcount = None + self._bigthumbhd = None + self._bestthumb = None + self._mix_pl = None + self.expiry = None + + if basic: + self._fetch_basic() + + if gdata: + self._fetch_gdata() + + if size: + for s in self.allstreams: + # pylint: disable=W0104 + s.get_filesize() + + + def _fetch_basic(self): + """ Fetch basic data and streams. """ + raise NotImplementedError + + + def _fetch_gdata(self): + """ Extract gdata values, fetch gdata if necessary. """ + raise NotImplementedError + + + def _process_streams(self): + """ Create Stream object lists from internal stream maps. """ + raise NotImplementedError + + + def __repr__(self): + """ Print video metadata. Return utf8 string. """ + if self._have_basic: + info = [("Title", self.title), + ("Author", self.author), + ("ID", self.videoid), + ("Duration", self.duration), + ("Rating", self.rating), + ("Views", self.viewcount)] + + nfo = "\n".join(["%s: %s" % i for i in info]) + + else: + nfo = "Pafy object: %s [%s]" % (self.videoid, + self.title[:45] + "..") + + return nfo.encode("utf8", "replace") if pyver == 2 else nfo + + @property + def streams(self): + """ The streams for a video. Returns list.""" + if not self._streams: + self._process_streams() + + return self._streams + + @property + def allstreams(self): + """ All stream types for a video. Returns list. """ + if not self._allstreams: + self._process_streams() + + return self._allstreams + + @property + def audiostreams(self): + """ Return a list of audio Stream objects. """ + if not self._audiostreams: + self._process_streams() + + return self._audiostreams + + @property + def videostreams(self): + """ The video streams for a video. Returns list. """ + if not self._videostreams: + self._process_streams() + + return self._videostreams + + @property + def oggstreams(self): + """ Return a list of ogg encoded Stream objects. """ + if not self._oggstreams: + self._process_streams() + + return self._oggstreams + + @property + def m4astreams(self): + """ Return a list of m4a encoded Stream objects. """ + if not self._m4astreams: + self._process_streams() + + return self._m4astreams + + @property + def title(self): + """ Return YouTube video title as a string. """ + if not self._title: + self._fetch_basic() + + return self._title + + @property + def author(self): + """ The uploader of the video. Returns str. """ + if not self._author: + self._fetch_basic() + + return self._author + + @property + def rating(self): + """ Rating for a video. Returns float. """ + if not self._rating: + self._fetch_basic() + + return self._rating + + @property + def length(self): + """ Length of a video in seconds. Returns int. """ + if not self._length: + self._fetch_basic() + + return self._length + + @property + def viewcount(self): + """ Number of views for a video. Returns int. """ + if not self._viewcount: + self._fetch_basic() + + return self._viewcount + + @property + def bigthumb(self): + """ Large thumbnail image url. Returns str. """ + self._fetch_basic() + return self._bigthumb + + @property + def bigthumbhd(self): + """ Extra large thumbnail image url. Returns str. """ + self._fetch_basic() + return self._bigthumbhd + + @property + def duration(self): + """ Duration of a video (HH:MM:SS). Returns str. """ + if not self._length: + self._fetch_basic() + + self._duration = time.strftime('%H:%M:%S', time.gmtime(self._length)) + self._duration = uni(self._duration) + + return self._duration + + @property + def keywords(self): + """ Return keywords as list of str. """ + if not self._keywords: + self._fetch_gdata() + + return self._keywords + + @property + def category(self): + """ YouTube category of the video. Returns string. """ + if not self._category: + self._fetch_gdata() + + return self._category + + @property + def description(self): + """ Description of the video. Returns string. """ + if not self._description: + self._fetch_gdata() + + return self._description + + @property + def username(self): + """ Return the username of the uploader. """ + if not self._username: + self._fetch_basic() + + return self._username + + @property + def published(self): + """ The upload date and time of the video. Returns string. """ + if not self._published: + self._fetch_gdata() + + return self._published.replace(".000Z", "").replace("T", " ") + + @property + def likes(self): + """ The number of likes for the video. Returns int. """ + if not self._likes: + self._fetch_basic() + + return self._likes + + @property + def dislikes(self): + """ The number of dislikes for the video. Returns int. """ + if not self._dislikes: + self._fetch_basic() + + return self._dislikes + + def _getbest(self, preftype="any", ftypestrict=True, vidonly=False): + """ + Return the highest resolution video available. + + Select from video-only streams if vidonly is True + """ + streams = self.videostreams if vidonly else self.streams + + if not streams: + return None + + def _sortkey(x, key3d=0, keyres=0, keyftype=0): + """ sort function for max(). """ + key3d = "3D" not in x.resolution + keyres = int(x.resolution.split("x")[0]) + keyftype = preftype == x.extension + strict = (key3d, keyftype, keyres) + nonstrict = (key3d, keyres, keyftype) + return strict if ftypestrict else nonstrict + + r = max(streams, key=_sortkey) + + if ftypestrict and preftype != "any" and r.extension != preftype: + return None + + else: + return r + + def getbestvideo(self, preftype="any", ftypestrict=True): + """ + Return the best resolution video-only stream. + + set ftypestrict to False to return a non-preferred format if that + has a higher resolution + """ + return self._getbest(preftype, ftypestrict, vidonly=True) + + def getbest(self, preftype="any", ftypestrict=True): + """ + Return the highest resolution video+audio stream. + + set ftypestrict to False to return a non-preferred format if that + has a higher resolution + """ + return self._getbest(preftype, ftypestrict, vidonly=False) + + def getbestaudio(self, preftype="any", ftypestrict=True): + """ Return the highest bitrate audio Stream object.""" + if not self.audiostreams: + return None + + def _sortkey(x, keybitrate=0, keyftype=0): + """ Sort function for max(). """ + keybitrate = int(x.rawbitrate) + keyftype = preftype == x.extension + strict, nonstrict = (keyftype, keybitrate), (keybitrate, keyftype) + return strict if ftypestrict else nonstrict + + r = max(self.audiostreams, key=_sortkey) + + if ftypestrict and preftype != "any" and r.extension != preftype: + return None + + else: + return r + + @classmethod + def _content_available(cls, url): + try: + response = urlopen(url) + except HTTPError: + return False + else: + return response.getcode() < 300 + + def getbestthumb(self): + """ Return the best available thumbnail.""" + if not self._bestthumb: + part_url = "http://i.ytimg.com/vi/%s/" % self.videoid + # Thumbnail resolution sorted in descending order + thumbs = ("maxresdefault.jpg", + "sddefault.jpg", + "hqdefault.jpg", + "mqdefault.jpg", + "default.jpg") + for thumb in thumbs: + url = part_url + thumb + if self._content_available(url): + return url + + return self._bestthumb + + def populate_from_playlist(self, pl_data): + """ Populate Pafy object with items fetched from playlist data. """ + self._title = pl_data.get("title") + self._author = pl_data.get("author") + self._length = int(pl_data.get("length_seconds", 0)) + self._rating = pl_data.get("rating", 0.0) + self._viewcount = "".join(re.findall(r"\d", "{0}".format(pl_data.get("views", "0")))) + self._viewcount = int(self._viewcount) + self._description = pl_data.get("description") + + +class BaseStream(object): + + """ YouTube video stream class. """ + + def __init__(self, parent): + """ Set initial values. """ + self._itag = None + self._mediatype = None + self._threed = None + self._rawbitrate = None + self._resolution = None + self._quality = None + self._dimensions = None + self._bitrate = None + self._extension = None + self.encrypted = None + self._notes = None + self._url = None + self._rawurl = None + + self._parent = parent + self._filename = None + self._fsize = None + self._active = False + + + @property + def rawbitrate(self): + """ Return raw bitrate value. """ + return self._rawbitrate + + @property + def threed(self): + """ Return bool, True if stream is 3D. """ + return self._threed + + @property + def itag(self): + """ Return itag value of stream. """ + return self._itag + + @property + def resolution(self): + """ Return resolution of stream as str. 0x0 if audio. """ + return self._resolution + + @property + def dimensions(self): + """ Return dimensions of stream as tuple. (0, 0) if audio. """ + return self._dimensions + + @property + def quality(self): + """ Return quality of stream (bitrate or resolution). + + eg, 128k or 640x480 (str) + """ + return self._quality + + @property + def title(self): + """ Return YouTube video title as a string. """ + return self._parent.title + + @property + def extension(self): + """ Return appropriate file extension for stream (str). + + Possible values are: 3gp, m4a, m4v, mp4, webm, ogg + """ + return self._extension + + @property + def bitrate(self): + """ Return bitrate of an audio stream. """ + return self._bitrate + + @property + def mediatype(self): + """ Return mediatype string (normal, audio or video). + + (normal means a stream containing both video and audio.) + """ + return self._mediatype + + @property + def notes(self): + """ Return additional notes regarding the stream format. """ + return self._notes + + @property + def url(self): + """ Return the url, decrypt if required. """ + return self._url + + @property + def url_https(self): + """ Return https url. """ + return self.url.replace("http://", "https://") + + def __repr__(self): + """ Return string representation. """ + out = "%s:%s@%s" % (self.mediatype, self.extension, self.quality) + return out + + def cancel(self): + """ Cancel an active download. """ + if self._active: + self._active = False + return True + +def remux(infile, outfile, quiet=False, muxer="ffmpeg"): + """ Remux audio. """ + muxer = muxer if isinstance(muxer, str) else "ffmpeg" + + for tool in set([muxer, "ffmpeg", "avconv"]): + cmd = [tool, "-y", "-i", infile, "-acodec", "copy", "-vn", outfile] + + try: + with open(os.devnull, "w") as devnull: + subprocess.call(cmd, stdout=devnull, stderr=subprocess.STDOUT) + + except OSError: + dbg("Failed to remux audio using %s", tool) + + else: + os.unlink(infile) + dbg("remuxed audio file using %s" % tool) + + if not quiet: + sys.stdout.write("\nAudio remuxed.\n") + + break + + else: + logging.warning("audio remux failed") + os.rename(infile, outfile) + + +def get_size_done(bytesdone, progress): + _progress_dict = {'KB': 1024.0, 'MB': 1048576.0, 'GB': 1073741824.0} + return round(bytesdone/_progress_dict.get(progress, 1.0), 2) + + +def get_status_string(progress): + status_string = (' {:,} ' + progress + ' [{:.2%}] received. Rate: [{:4.0f} ' + 'KB/s]. ETA: [{:.0f} secs]') + + if early_py_version: + status_string = (' {0:} ' + progress + ' [{1:.2%}] received. Rate:' + ' [{2:4.0f} KB/s]. ETA: [{3:.0f} secs]') + + return status_string diff --git a/horsydist/resources/pafy_fix/backend_youtube_dl.py b/horsydist/resources/pafy_fix/backend_youtube_dl.py new file mode 100644 index 0000000..fa8f501 --- /dev/null +++ b/horsydist/resources/pafy_fix/backend_youtube_dl.py @@ -0,0 +1,183 @@ +import sys +import time +import logging +import os +import subprocess + +if sys.version_info[:2] >= (3, 0): + # pylint: disable=E0611,F0401,I0011 + uni = str +else: + uni = unicode + +import youtube_dl + +from .backend_shared import BasePafy, BaseStream, remux, get_status_string, get_size_done + +dbg = logging.debug + + +early_py_version = sys.version_info[:2] < (2, 7) + + +class YtdlPafy(BasePafy): + def __init__(self, *args, **kwargs): + self._ydl_info = None + self._ydl_opts = {'quiet': True, 'prefer_insecure': False, 'no_warnings': True} + ydl_opts = kwargs.get("ydl_opts") + if ydl_opts: + self._ydl_opts.update(ydl_opts) + super(YtdlPafy, self).__init__(*args, **kwargs) + + def _fetch_basic(self): + """ Fetch basic data and streams. """ + if self._have_basic: + return + + with youtube_dl.YoutubeDL(self._ydl_opts) as ydl: + try: + self._ydl_info = ydl.extract_info(self.videoid, download=False) + # Turn into an IOError since that is what pafy previously raised + except youtube_dl.utils.DownloadError as e: + raise IOError(str(e).replace('YouTube said', 'Youtube says')) + + if self.callback: + self.callback("Fetched video info") + + self._title = self._ydl_info['title'] + self._author = self._ydl_info['uploader'] + self._rating = self._ydl_info['average_rating'] + self._length = self._ydl_info['duration'] + self._viewcount = self._ydl_info['view_count'] + self._username = self._ydl_info['uploader_id'] + self._category = self._ydl_info['categories'][0] if self._ydl_info['categories'] else '' + self._bestthumb = self._ydl_info['thumbnails'][0]['url'] + self._bigthumb = "http://i.ytimg.com/vi/%s/mqdefault.jpg" % self.videoid + self._bigthumbhd = "http://i.ytimg.com/vi/%s/hqdefault.jpg" % self.videoid + self.expiry = time.time() + 60 * 60 * 5 + + self._have_basic = True + + def _fetch_gdata(self): + """ Extract gdata values, fetch gdata if necessary. """ + if self._have_gdata: + return + + item = self._get_video_gdata(self.videoid)['items'][0] + snippet = item['snippet'] + self._published = uni(snippet['publishedAt']) + self._description = uni(snippet["description"]) + # Note: using snippet.get since some videos have no tags object + self._keywords = [uni(i) for i in snippet.get('tags', ())] + self._have_gdata = True + + def _process_streams(self): + """ Create Stream object lists from internal stream maps. """ + + if not self._have_basic: + self._fetch_basic() + + allstreams = [YtdlStream(z, self) for z in self._ydl_info['formats']] + self._streams = [i for i in allstreams if i.mediatype == 'normal'] + self._audiostreams = [i for i in allstreams if i.mediatype == 'audio'] + self._videostreams = [i for i in allstreams if i.mediatype == 'video'] + self._m4astreams = [i for i in allstreams if i.extension == 'm4a'] + self._oggstreams = [i for i in allstreams if i.extension == 'ogg'] + self._allstreams = allstreams + + +class YtdlStream(BaseStream): + def __init__(self, info, parent): + super(YtdlStream, self).__init__(parent) + self._itag = info['format_id'] + + if (info.get('acodec') != 'none' and + info.get('vcodec') == 'none'): + self._mediatype = 'audio' + elif (info.get('acodec') == 'none' and + info.get('vcodec') != 'none'): + self._mediatype = 'video' + else: + self._mediatype = 'normal' + + self._threed = info.get('format_note') == '3D' + self._rawbitrate = info.get('abr', 0) * 1024 + + height = info.get('height') or 0 + width = info.get('width') or 0 + self._resolution = str(width) + 'x' + str(height) + self._dimensions = width, height + self._bitrate = str(info.get('abr', 0)) + 'k' + self._quality = self._bitrate if self._mediatype == 'audio' else self._resolution + + self._extension = info['ext'] + self._notes = info.get('format_note') or '' + self._url = info.get('url') + + self._info = info + + def get_filesize(self): + """ Return filesize of the stream in bytes. Set member variable. """ + + # Faster method + if 'filesize' in self._info and self._info['filesize'] is not None: + return self._info['filesize'] + + # Fallback + return super(YtdlStream, self).get_filesize() + + def download(self, filepath="", quiet=False, progress="Bytes", + callback=None, meta=False, remux_audio=False): + + downloader = youtube_dl.downloader.http.HttpFD(ydl(), + {'http_chunk_size': 10485760}) + + progress_available = ["KB", "MB", "GB"] + if progress not in progress_available: + progress = "Bytes" + + status_string = get_status_string(progress) + + def progress_hook(s): + if s['status'] == 'downloading': + bytesdone = s['downloaded_bytes'] + total = s['total_bytes'] + if s.get('speed') is not None: + rate = s['speed'] / 1024 + else: + rate = 0 + if s.get('eta') is None: + eta = 0 + else: + eta = s['eta'] + + progress_stats = (get_size_done(bytesdone, progress), + bytesdone*1.0/total, rate, eta) + if not quiet: + status = status_string.format(*progress_stats) + sys.stdout.write("\r" + status + ' ' * 4 + "\r") + sys.stdout.flush() + + if callback: + callback(total, *progress_stats) + + downloader._progress_hooks = [progress_hook] + + if filepath and os.path.isdir(filepath): + filename = self.generate_filename(max_length=256 - len('.temp')) + filepath = os.path.join(filepath, filename) + + elif filepath: + pass + + else: + filepath = self.generate_filename(meta=meta, max_length=256 - len('.temp')) + + infodict = {'url': self.url} + + downloader.download(filepath, infodict) + print() + + if remux_audio and self.mediatype == "audio": + subprocess.run(['mv', filepath, filepath + '.temp']) + remux(filepath + '.temp', filepath, quiet=quiet, muxer=remux_audio) diff --git a/horsydist/resources/pafy_fix/pafy.py b/horsydist/resources/pafy_fix/pafy.py new file mode 100644 index 0000000..5575760 --- /dev/null +++ b/horsydist/resources/pafy_fix/pafy.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +Pafy = None + +def new(url, basic=True, gdata=False, size=False, + callback=None, ydl_opts=None): + global Pafy + if Pafy is None: + from .backend_youtube_dl import YtdlPafy as Pafy + + return Pafy(url, basic, gdata, size, callback, ydl_opts=ydl_opts)