diff --git a/README.md b/README.md new file mode 100644 index 0000000..acd995a --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +plugin.video.ardmediathek_de +============================ + +Information: http://wiki.xbmc.org/index.php?title=Add-on:ARD_Mediathek +Features: Streaming and downloading videos from http://www.ardmediathek.de/ diff --git a/addon.xml b/addon.xml index fb80859..0a57c27 100644 --- a/addon.xml +++ b/addon.xml @@ -2,6 +2,7 @@ + video diff --git a/default.py b/default.py index 7074fab..03fcb99 100644 --- a/default.py +++ b/default.py @@ -19,6 +19,10 @@ forceViewMode = addon.getSetting("forceViewMode") == "true" useThumbAsFanart=addon.getSetting("useThumbAsFanart") == "true" viewMode = str(addon.getSetting("viewMode")) +errorMessageDurationSec = 20 +characterEncoding = "iso-8859-15" +configEnableDownloadFeature = False + baseUrl = "http://www.ardmediathek.de" defaultThumb = baseUrl+"/ard/static/pics/default/16_9/default_webM_16_9.jpg" defaultBackground = "http://www.ard.de/pool/img/ard/background/base_xl.jpg" @@ -85,7 +89,7 @@ def listDossiers(): url = baseUrl+match[0] id = url[url.find("documentId=")+11:] url = baseUrl+"/ard/servlet/ajax-cache/3517004/view=list/documentId="+id+"/goto=1/index.html" - match = re.compile('\n (.+?)\n', re.DOTALL).findall(entry) + match = re.compile('\n (.+?)\n', re.DOTALL).findall(entry) title = cleanTitle(match[0]) match = re.compile('src="(.+?)"', re.DOTALL).findall(entry) thumb = getBetterThumb(baseUrl+match[0]) @@ -96,33 +100,7 @@ def listDossiers(): def listShowVideos(url): - content = getUrl(url) - spl = content.split('
') - for i in range(1, len(spl), 1): - entry = spl[i] - if "mt-icon_video" in entry: - match = re.compile('(.+?)', re.DOTALL).findall(entry) - url = baseUrl+match[0][0] - title = cleanTitle(match[0][2]) - match = re.compile('\n (.+?)\n (.+?) min\n ', re.DOTALL).findall(entry) - duration = "" - if match: - date = match[0][0] - duration = match[0][1] - title = date[:5]+" - "+title - if "00:" in duration: - duration = 1 - match = re.compile('src="(.+?)"', re.DOTALL).findall(entry) - thumb = getBetterThumb(baseUrl+match[0]) - if "Livestream" not in title: - addLink(title, url, 'playVideo', thumb, duration) - match = re.compile('(.+?)', re.DOTALL).findall(content) - for url, title in match: - if title == "Weiter": - addDir(translation(30009), baseUrl+url, 'listShowVideos', "", "") - xbmcplugin.endOfDirectory(pluginhandle) - if forceViewMode: - xbmc.executebuiltin('Container.SetViewMode('+viewMode+')') + iterateContent(url, True, 'listShowVideos', createVideoEntry) def listShowsAZMain(): @@ -150,6 +128,10 @@ def listShowsAZ(letter): if forceViewMode: xbmc.executebuiltin('Container.SetViewMode('+viewMode+')') + +# +# Determines URL of better thumb for a video +# def getBetterThumb(url): if baseUrl+"/ard/static/pics/default/16_9/default" in url: url = defaultThumb @@ -177,7 +159,94 @@ def getBetterThumb(url): id = str(id) url = baseUrl+"/ard/servlet/contentblob/"+newID[0:2]+"/"+newID[2:4]+"/"+newID[4:6]+"/"+newID[6:8]+"/"+id+"/bild/1" return url + + +# +# Iterates over a web page and identifies all listed content elements (e.g. video/audio) +# +def iterateContent(url, videoOnly, nextPageAction, callbackForVideo, separator='
'): + #print("list content for " +url) + content = getUrl(url) + # find relevant parts of web page + spl = content.split(separator) + for i in range(1, len(spl), 1): + entry = spl[i] + useEntry = True; + if videoOnly: + # there are also "mt-icon_audio" entries, which does not contain the following marker + if "mt-icon_video" not in entry: + useEntry = False + if useEntry: + title, pageurl, thumb, duration, channel, show, desc, date = extractVideoDescription(entry) + # was analysis successful? + if title and pageurl: + #print("Content entry: " +title +" (" +pageurl +") " +duration +" - " +channel +" - " +show +" - descr:" +desc +" - date:" +date) + callbackForVideo(title, pageurl, thumb, duration, channel, show, desc, date) + else: + # error handling for web page without any information + print("Ignoring entry without title and URL.") + # have a look for some "next page" indicator + if nextPageAction: + # searching for something like + # 'Weiter' + match = re.compile(']*rel="[0-9]+"[^<>]*>(.+?)', re.DOTALL).findall(content) + for url, title in match: + if title == "Weiter": + addDir(translation(30009), baseUrl+url, nextPageAction, "", "") + # end list + xbmcplugin.endOfDirectory(pluginhandle) + if forceViewMode: + xbmc.executebuiltin('Container.SetViewMode('+viewMode+')') + + +# +# Analyzes the content of a web page and try to extract important information about a video +# Returns multiple variables (equal to empty string if not found in the web page): +# 1. title +# 2. pageurl +# 3. thumb +# 4. duration +# 5. channel +# 6. show +# 7. desc +# 8. date +# +def extractVideoDescription(entry): + # init result + title = pageurl = thumb = duration = channel = show = desc = date = "" + # base information + match = re.compile('(.+?)', re.DOTALL).findall(entry) + if match: + pageurl = baseUrl+match[0][0] + title = cleanTitle(match[0][2]) + # show + match = re.compile('

aus: (.+?)

', re.DOTALL).findall(entry) + show = "" + if match: + show = match[0] + # channel + match = re.compile('(.+?)', re.DOTALL).findall(entry) + channel = "" + if match: + channel = match[0] + # duration and date + # (encoded as "15.01.14 16:46 min" or in the search results as "15.01.14 - 16:46 min" + match = re.compile('([0-9\.]+)[^0-9]*([0-9:]+ min)?', re.DOTALL).findall(entry) + if match: + date = match[0][0] + duration = match[0][1] + duration = duration.replace("min", "").strip() + if "00:" in duration: + # XBMC will round this duration to zero. Thus, we set it to at least one minute + # (do not return an int, because it is normally a string) + duration = "01:00" + # thumbs + match = re.compile('src="(.+?)"', re.DOTALL).findall(entry) + thumb = getBetterThumb(baseUrl+match[0]) + + return title, pageurl, thumb, duration, channel, show, desc, date + def listCats(): content = getUrl(baseUrl) @@ -198,87 +267,28 @@ def listVideosMain(id): def listVideos(url): - content = getUrl(url) - spl = content.split('
') - for i in range(1, len(spl), 1): - entry = spl[i] - if "mt-icon_video" in entry: - match = re.compile('(.+?)', re.DOTALL).findall(entry) - url = baseUrl+match[0][0] - title = cleanTitle(match[0][2]) - match = re.compile('

aus: (.+?)

', re.DOTALL).findall(entry) - show = "" - if match: - show = match[0] - match = re.compile('(.+?)', re.DOTALL).findall(entry) - channel = "" - if match: - channel = match[0] - match = re.compile('\n (.+?)\n (.+?) min\n ', re.DOTALL).findall(entry) - duration = "" - date = "" - if match: - date = match[0][0] - duration = match[0][1] - title = date[:5]+" - "+title - if "00:" in duration: - duration = 1 - match = re.compile('src="(.+?)"', re.DOTALL).findall(entry) - thumb = getBetterThumb(baseUrl+match[0]) - desc = cleanTitle(date+" - "+show+" ("+channel+")") - if "Livestream" not in title: - addLink(title, url, 'playVideo', thumb, duration, desc) - xbmcplugin.endOfDirectory(pluginhandle) - if forceViewMode: - xbmc.executebuiltin('Container.SetViewMode('+viewMode+')') + iterateContent(url, True, None, createVideoEntry) def listVideosDossier(url): + iterateContent(url, False, 'listVideosDossier', createVideoEntry) + + +# +# Extracts the stream URL from a web page +# +# Returns two results: +# 1. stream URL or None on error +# 2. error message in case of error +# +def extractStreamURL(url): + # get web page content = getUrl(url) - spl = content.split('
') - for i in range(1, len(spl), 1): - entry = spl[i] - if 'class="mt-fo_source"' in entry: - match = re.compile('(.+?)', re.DOTALL).findall(entry) - url = baseUrl+match[0][0] - title = cleanTitle(match[0][2]) - match = re.compile('

aus: (.+?)

', re.DOTALL).findall(entry) - show = "" - if match: - show = match[0] - match = re.compile('(.+?)', re.DOTALL).findall(entry) - channel = "" - if match: - channel = match[0] - match = re.compile('\n (.+?)\n (.+?) min\n ', re.DOTALL).findall(entry) - duration = "" - date = "" - if match: - date = match[0][0] - duration = match[0][1] - title = date[:5]+" - "+title - if "00:" in duration: - duration = 1 - match = re.compile('src="(.+?)"', re.DOTALL).findall(entry) - thumb = getBetterThumb(baseUrl+match[0]) - desc = cleanTitle(date+" - "+show+" ("+channel+")") - if "Livestream" not in title: - addLink(title, url, 'playVideo', thumb, duration, desc) - match = re.compile('(.+?)', re.DOTALL).findall(content) - for url, title in match: - if title == "Weiter": - addDir(translation(30009), baseUrl+url, 'listVideosDossier', "", "") - xbmcplugin.endOfDirectory(pluginhandle) - if forceViewMode: - xbmc.executebuiltin('Container.SetViewMode('+viewMode+')') - - -def playVideo(url): - content = getUrl(url) + # analyse the web page matchFSK = re.compile('
(.+?)
', re.DOTALL).findall(content) if matchFSK: fsk = matchFSK[0].strip() - xbmc.executebuiltin('XBMC.Notification(Info:,'+fsk+',15000)') + return None, fsk else: match5 = re.compile('addMediaStream\\(1, 2, "", "(.+?)"', re.DOTALL).findall(content) match6 = re.compile('addMediaStream\\(1, 1, "", "(.+?)"', re.DOTALL).findall(content) @@ -287,7 +297,7 @@ def playVideo(url): match3 = re.compile('addMediaStream\\(0, 1, "(.+?)", "(.+?)"', re.DOTALL).findall(content) match4 = re.compile('addMediaStream\\(0, 1, "", "(.+?)"', re.DOTALL).findall(content) matchUT = re.compile('setSubtitleUrl\\("(.+?)"', re.DOTALL).findall(content) - url = "" + url = None if match5: url = match5[0] elif match6: @@ -309,10 +319,85 @@ def playVideo(url): if url: if "?" in url: url = url[:url.find("?")] - listitem = xbmcgui.ListItem(path=url) - xbmcplugin.setResolvedUrl(pluginhandle, True, listitem) - if showSubtitles and matchUT: - setSubtitle(baseUrl+matchUT[0]) + return url, None + else: + return None, None + + +# +# Start playing a stream by extracting the stream URL from a web page URL +# +def playVideo(url): + streamURL, errorMsg = extractStreamURL(url) + if streamURL: + print("Playing " +streamURL +" (web page = " +url +")") + listitem = xbmcgui.ListItem(path=streamURL) + xbmcplugin.setResolvedUrl(pluginhandle, True, listitem) + if showSubtitles and matchUT: + setSubtitle(baseUrl+matchUT[0]) + else: + if not errorMsg: + errorMsg = url; + reportError(translation(30200), translation(30202) +" " +errorMsg) + + +# +# Enqueues a video to the download list of the plugin SimpleDownloader +# +def downloadVideo(url, name): + # determine stream URL from web page URL + streamURL, errorMsg = extractStreamURL(url) + + # handle error case + if not streamURL: + # avoid encoding problems within string operation in call to reportError + if isinstance(errorMsg, unicode): + errorMsg = errorMsg.encode(characterEncoding, "replace") + baseMsg = translation(30202) + if isinstance(baseMsg, unicode): + baseMsg = baseMsg.encode(characterEncoding, "replace") + reportError(translation(30200), baseMsg +" " +errorMsg) + return + + print("Download '" +name +"' from '" +streamURL +"' (web page url = '" +url +"')") + + # use original file name as suffix in order to (a) have correct extension + # for file and (b) to enable multiple files with same base name (e.g. + # different format/resolutions) + nameClean = name +" - " +os.path.basename(streamURL) + # clean up name to ASCII characters, because it is used as file name later on + # remove any problematic characters (e.g., 'ü') + regex = re.compile('[^a-zA-Z0-9_\.\- ]+') + nameClean = regex.sub('_', nameClean).strip() + + # check name of destination folder (from plug-in configuration) + downloadFolder = addon.getSetting("downloadFolder") + if downloadFolder: + print("Download '" +nameClean +"' to folder '" +downloadFolder +"'") + else: + reportError(translation(30200), translation(30203)) + return + + # init SimpleDownloader plugin + downloader = None + try: + import SimpleDownloader + downloader = SimpleDownloader.SimpleDownloader() + except: + reportError(translation(30200), translation(30201)) + return + + # debugging: remove invalid old entries from persistent queue + #downloader.cleanQueue() + + # set parameters for download + dparams = {} + dparams["Title"] = name + dparams["url"] = streamURL + dparams["download_path"] = downloadFolder + + # start download itself + downloader.download(nameClean, dparams) def setSubtitle(url): @@ -366,40 +451,7 @@ def search(): def listVideosSearch(url): - content = getUrl(url) - spl = content.split('
') - for i in range(1, len(spl), 1): - entry = spl[i] - match = re.compile('(.+?)', re.DOTALL).findall(entry) - url = baseUrl+match[0][0] - title = cleanTitle(match[0][2]) - match = re.compile('

aus: (.+?)

', re.DOTALL).findall(entry) - show = "" - if match: - show = match[0] - match = re.compile('(.+?)', re.DOTALL).findall(entry) - channel = "" - if match: - channel = match[0] - match = re.compile('(.+?) · (.+?) min', re.DOTALL).findall(entry) - duration = "" - date = "" - if match: - date = match[0][0] - duration = match[0][1] - title = date[:5]+" - "+title - match = re.compile('src="(.+?)"', re.DOTALL).findall(entry) - thumb = getBetterThumb(baseUrl+match[0]) - desc = cleanTitle(date+" - "+show+" ("+channel+")") - if "Livestream" not in title: - addLink(title, url, 'playVideo', thumb, duration, desc) - match = re.compile('(.+?)', re.DOTALL).findall(content) - for url, title in match: - if title == "Weiter": - addDir(translation(30009), baseUrl+url.replace("&", "&"), 'listVideosSearch', "", "") - xbmcplugin.endOfDirectory(pluginhandle) - if forceViewMode == True: - xbmc.executebuiltin('Container.SetViewMode('+viewMode+')') + iterateContent(url, False, 'listVideosSearch', createVideoEntry, separator='
') def cleanTitle(title): @@ -464,12 +516,32 @@ def parameters_string_to_dict(parameters): paramDict[paramSplits[0]] = paramSplits[1] return paramDict +# +# Callback per video for list function +# (at least title and pageurl have to be valid) +# +def createVideoEntry(title, pageurl, thumb, duration, channel, show, desc, date): + if "Livestream" not in title: + if not desc: + desc = cleanTitle(date+" - "+show+" ("+channel+")") + if show and addon.getSetting("showSeriesInTitle") == "true": + title = show +": " +title + if date and addon.getSetting("showDateInTitle") == "true": + # add date to title + title = date[:6]+" "+title + addLink(cleanTitle(title), pageurl, 'playVideo', thumb, duration, desc) + def addLink(name, url, mode, iconimage, duration="", desc=""): u = sys.argv[0]+"?url="+urllib.quote_plus(url)+"&mode="+str(mode) ok = True liz = xbmcgui.ListItem(name, iconImage=defaultThumb, thumbnailImage=iconimage) - liz.setInfo(type="Video", infoLabels={"Title": name, "Duration": duration, "Plot": desc}) + infos = {"Title": name} + if duration: + infos["Duration"] = duration + if desc: + infos["Plot"] = desc + liz.setInfo(type="Video", infoLabels=infos) liz.setProperty('IsPlayable', 'true') if useThumbAsFanart: if not iconimage or iconimage==icon: @@ -478,6 +550,9 @@ def addLink(name, url, mode, iconimage, duration="", desc=""): else: liz.setProperty("fanart_image", defaultBackground) liz.addContextMenuItems([(translation(30012), 'RunPlugin(plugin://'+addonID+'/?mode=queueVideo&url='+urllib.quote_plus(u)+'&name='+urllib.quote_plus(name)+')',)]) + if configEnableDownloadFeature: + liz.addContextMenuItems([(translation(30040), 'RunPlugin(plugin://'+addonID+'/?mode=downloadVideo&url='+urllib.quote_plus(url)+'&name='+urllib.quote_plus(name)+')',)]) + # else: downloading is not allowed ok = xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=u, listitem=liz) return ok @@ -530,6 +605,22 @@ def addShowFavDir(name, url, mode, iconimage): ok = xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=u, listitem=liz, isFolder=True) return ok + +# +# Saves an error message in the log file and outputs it to the user on the screen +# +def reportError(title, msg): + # convert to unicode in order to avoid later exceptions + if isinstance(title, unicode): + title = title.encode(characterEncoding, "replace") + if isinstance(msg, unicode): + msg = msg.encode(characterEncoding, "replace") + # save it in log file + print(title +": " +msg) + # display it + xbmc.executebuiltin('XBMC.Notification(' +title +',' +msg +',' +str(errorMessageDurationSec *1000) +')') + + params = parameters_string_to_dict(sys.argv[2]) mode = urllib.unquote_plus(params.get('mode', '')) url = urllib.unquote_plus(params.get('url', '')) @@ -561,6 +652,8 @@ def addShowFavDir(name, url, mode, iconimage): listShowVideos(url) elif mode == 'playVideo': playVideo(url) +elif mode == 'downloadVideo': + downloadVideo(url, name) elif mode == "queueVideo": queueVideo(url, name) elif mode == 'playLive': diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml index e15f651..476a876 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -22,8 +22,18 @@ Add to addon favs Remove from addon favs Play alternative Stream-URL + Download video + Force View ViewID Use thumb as fanart Activate subtitles (if available) + Destination folder for downloaded videos + Show date in video title + Show name of series in video title + + Error + Plug-in SimpleDownloader required for downloading not found. Maybe it has to be installed first. + Stream URL can not be determined. Error: + Destination folder not defined. Please define it in the configuration section of this plug-in. diff --git a/resources/language/German/strings.xml b/resources/language/German/strings.xml index 055e193..7f5d2d1 100644 --- a/resources/language/German/strings.xml +++ b/resources/language/German/strings.xml @@ -22,7 +22,17 @@ Zu Addon Favs hinzufügen Aus Addon Favs entfernen Alternative Stream-Url abspielen + Video herunterladen + View erzwingen Thumb als Fanart nutzen Untertitel aktivieren (falls verfügbar) + Zielordner für heruntergeladene Videos + Zeige Datum im Videotitel + Zeige Serienname im Videotitel + + Fehler + Zum Herunterladen notwendiges Plugin SimpleDownloader nicht gefunden. Es muss möglicherweise erst installiert werden. + Stream-URL kann nicht bestimmt werden. Fehler: + Zielverzeichnis ist nicht definiert. Bitte Konfiguration des Plug-ins anpassen. diff --git a/resources/settings.xml b/resources/settings.xml index 8132dea..f2e1445 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -3,4 +3,7 @@ + + +