summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/bash/environment.sh3
-rw-r--r--.local/share/libquvi-scripts/media/zdfmediathek.lua308
2 files changed, 311 insertions, 0 deletions
diff --git a/.config/bash/environment.sh b/.config/bash/environment.sh
index 7cec917..a830310 100644
--- a/.config/bash/environment.sh
+++ b/.config/bash/environment.sh
@@ -102,4 +102,7 @@ export EIXRC="${XDG_CONFIG_HOME}/eixrc/eixrc"
export DVDCSS_CACHE="${XDG_CACHE_HOME}/dvdcss/"
+# Needed for libquvi 0.9 (0.4 automatically searches here)
+export LIBQUVI_SCRIPTS_DIR="${HOME}/.local/share/libquvi-scripts/"
+
#export BROWSER=luakit
diff --git a/.local/share/libquvi-scripts/media/zdfmediathek.lua b/.local/share/libquvi-scripts/media/zdfmediathek.lua
new file mode 100644
index 0000000..3569708
--- /dev/null
+++ b/.local/share/libquvi-scripts/media/zdfmediathek.lua
@@ -0,0 +1,308 @@
+-- libquvi-scripts
+-- Copyright (C) 2014 Benjamin Franzke <benjaminfranzke@googlemail.com>
+--
+-- This file is part of libquvi-scripts <http://quvi.sourceforge.net/>.
+--
+-- This program is free software: you can redistribute it and/or
+-- modify it under the terms of the GNU Affero General Public
+-- License as published by the Free Software Foundation, either
+-- version 3 of the License, or (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU Affero General Public License for more details.
+--
+-- You should have received a copy of the GNU Affero General
+-- Public License along with this program. If not, see
+-- <http://www.gnu.org/licenses/>.
+--
+
+local ZDFmediathek = {} -- Utility functions unique to this script.
+
+-- Identify the script.
+function ident(qargs)
+ return {
+ can_parse_url = ZDFmediathek.can_parse_url(qargs),
+ domains = table.concat({'zdf.de'}, ',')
+ }
+end
+
+function ZDFmediathek.can_parse_url(qargs)
+ local U = require 'socket.url'
+ local t = U.parse(qargs.input_url)
+
+ if t and t.scheme and t.scheme:lower():match('^http$')
+ and t.host and t.host:lower():match('zdf%.de$')
+ and t.path and t.path:match('/ZDFmediathek/')
+ then
+ return true
+ else
+ return false
+ end
+end
+
+ZDFmediathek.xmlservice_schema = {
+ status = { statuscode = false, debuginfo = true },
+ video = {
+ information = { title = false },
+ details = { lengthSec = true },
+ teaserimages = { teaserimage = {} },
+ formitaeten = {
+ formitaet = {
+ quality = false,
+ url = false,
+ width = true,
+ height = true,
+ videoBitrate = true,
+ audioBitrate = true,
+ facets = { facet = false }
+ }
+ }
+ }
+}
+
+function ZDFmediathek.find_tag(t, tag)
+ local r = {}
+ for i=1, #t do
+ if t[i].tag == tag then
+ table.insert(r, t[i])
+ end
+ end
+ return r
+end
+
+function ZDFmediathek.find_first_tag(t, tag, optional)
+ for i=1, #t do
+ if t[i].tag == tag then
+ return t[i]
+ end
+ end
+ if optional then
+ return {}
+ else
+ error("[find_first_tag] no match: tag=" .. tag)
+ end
+end
+
+function ZDFmediathek.extract_schema(x, s)
+ local t = {}
+
+ for k,v in pairs(s) do
+ if type(v) == "boolean" then
+ t[k] = ZDFmediathek.find_first_tag(x, k, v)[1]
+ elseif type(v) == "table" then
+ local tags = ZDFmediathek.find_tag(x, k)
+
+ t[k] = {}
+ for _,tag in ipairs(tags) do
+ if next(v) == nil then -- if v is empty insert the tag values
+ table.insert(t[k], tag[1])
+ else
+ local d = ZDFmediathek.extract_schema(tag, v)
+ d.attr = tag.attr
+ table.insert(t[k], d)
+ end
+ end
+
+ if #t[k] == 1 then t[k] = t[k][1] end
+ end
+ end
+
+ return t
+end
+
+-- Parse media URL.
+function parse(qargs)
+ local P = require 'lxp.lom'
+
+ qargs.id = qargs.input_url:match('/ZDFmediathek/#?/?beitrag/video/(%d+)') or
+ qargs.input_url:match('/ZDFmediathek/#?/?beitrag/live/(%d+)')
+ or error ("no match: media id")
+ local xml = { "http://www.zdf.de/",
+ "ZDFmediathek/xmlservice/web/beitragsDetails?id=", qargs.id }
+ local c = quvi.http.fetch(table.concat(xml)).data
+ local x = P.parse(c)
+ local r = ZDFmediathek.extract_schema(x, ZDFmediathek.xmlservice_schema)
+ io.stderr:write(dump(r) .. '\n')
+
+ if r.status.statuscode ~= "ok" then
+ error(table.concat({'error: ', r.status.statuscode, ' - ', r.status.debuginfo or ''}))
+ end
+
+ qargs.title = r.video.information.title
+ local imgs = r.video.teaserimages.teaserimage
+ qargs.thumb_url = imgs[#imgs]
+
+ qargs.duration_ms = 1000 * tonumber(r.video.details.lengthSec or '0')
+ qargs.streams = ZDFmediathek.iter_streams(r)
+
+ return qargs
+end
+
+function ZDFmediathek.add_stream(t, s)
+ local e = s.nostd
+ if e.facet == 'hbbtv' then return end -- Only for HBBTV useragents, drop it.
+
+ table.insert(t, s)
+
+ -- Some URLs/Formats are not explicitly listed but can be derived:
+
+ -- The "high"-quality f4m file may contain a reference to a file that is
+ -- of better quality than "veryhigh" classified http streams.
+ -- Example: 2256k instead of 1496k
+ if e.quality == "high" and e.type == "h264_aac_f4f_http_f4m_http" then
+ local new = { container = "mp4" ,
+ nostd = { type = "h264_aac_mp4_http_na_na" } }
+ new.url = ZDFmediathek.http_mp4_from_f4m(s.url)
+ if new.url then
+ table.insert(t, ZDFmediathek.merge_stream_descriptor(s, new))
+ end
+ end
+
+ -- HD quality movies are not listed with an http url.
+ -- But one can build it from the rtmp URL given in a .meta file:
+ if e.quality == "hd" and e.type == "h264_aac_mp4_rtmp_zdfmeta_http" then
+ local n = { container = "mp4",
+ nostd = { type = "h264_aac_mp4_http_na_na", protocol = "http" }}
+ new.url = ZDFmediathek.http_mp4_from_zdfmeta(s.url)
+ if new.url then
+ table.insert(t, ZDFmediathek.merge_stream_descriptor(s, new))
+ end
+ end
+end
+
+function ZDFmediathek.stream_apply_quirks(s, U)
+ if U.ends_with(s.url, 'manifest.f4m') then
+ s.url = s.url .. '?hdcore' -- The server returns 403 without ?hdcore
+ end
+ if s.nostd.type == 'h264_aac_na_rtsp_mov_http' then
+ s.nostd.type = 'h264_aac_3gp_rtsp_na_na' -- Add missing container specifier
+ end
+end
+
+function ZDFmediathek.iter_streams(r)
+ local S = require 'quvi/stream'
+ local U = require 'quvi/util'
+
+ local t = {}
+ for _,fmt in ipairs(r.video.formitaeten.formitaet) do
+ local s = S.stream_new(fmt.url)
+ s.nostd = { type = fmt.attr.basetype or error("missing type attribute"),
+ quality = fmt.quality }
+ ZDFmediathek.stream_apply_quirks(s, U)
+
+ s.video.width = tonumber(fmt.width or 0)
+ s.video.height = tonumber(fmt.height or 0)
+
+ s.video.bitrate_kbit_s = fmt.videoBitrate or ''
+ s.audio.bitrate_kbit_s = fmt.audioBitrate or ''
+
+ s.video.encoding, s.audio.encoding, s.container, s.nostd.protocol =
+ s.nostd.type:match("^(.-)_(.-)_(.-)_(.-)_.-")
+ s.nostd.facet = fmt.facets.facet or nil
+ ZDFmediathek.add_stream(t, s)
+ end
+
+ for _,v in pairs(t) do
+ v.id = ZDFmediathek.to_id(v)
+ end
+
+ table.sort(t, ZDFmediathek.compare_stream)
+ -- After sort, first stream is the best
+ t[1].flags.best = true
+
+ return t
+end
+
+function ZDFmediathek.http_mp4_from_zdfmeta(meta_url)
+ local c = quvi.http.fetch(meta_url).data
+ local path = c:match("mp4:(.-%.mp4)")
+
+ return path and "http://nrodl.zdf.de/none/" .. path or nil
+end
+
+function ZDFmediathek.http_mp4_from_f4m(f4m_url)
+ local c = quvi.http.fetch(f4m_url).data
+ local t = { max_rate = 0, path = nil }
+
+ for p, r in c:gmatch('href="(.-)/manifest%.f4m%?hdcore" bitrate="(%d+)"') do
+ if tonumber(r) > t.max_rate then
+ t = { max_rate = tonumber(r), path = p }
+ end
+ end
+
+ return t.path and "http://nrodl.zdf.de/none/" .. t.path or nil
+end
+
+function ZDFmediathek.rank_type(s)
+ local rank = {
+ h264_aac_mp4_http_na_na = 4,
+ vp8_vorbis_webm_http_na_na = 3,
+ h264_aac_3gp_rtsp_na_na = 2,
+ h264_aac_ts_http_m3u8_http = 1
+ }
+ return rank[s.nostd.type] and rank[s.nostd.type] or 0
+end
+
+function ZDFmediathek.rank_quality(s)
+ local rank = { hd = 5, veryhigh = 4, high = 3, med = 2, low = 1 }
+ return rank[s.nostd.quality] and rank[s.nostd.quality] or 0
+end
+
+function ZDFmediathek.compare_stream(s1, s2)
+ local t1 = ZDFmediathek.rank_type(s1)
+ local t2 = ZDFmediathek.rank_type(s2)
+
+ if (t1 ~= t2) then
+ return t1 > t2
+ end
+
+ local v = { s1.video, s2.video }
+ if v[1].width and v[2].width and v[1].height and v[2].height then
+ return v[1].width > v[2].width and v[1].height > v[2].height
+ end
+
+ return ZDFmediathek.rank_quality(s1) > ZDFmediathek.rank_quality(s2)
+end
+
+function ZDFmediathek.to_id(s)
+ local proto = (s.nostd.protocol ~= "http") and '_' .. s.nostd.protocol or ''
+ local quality = (s.video.height) and s.video.height ..'p' or s.nostd.quality
+ return table.concat({ s.container, proto, '_', quality })
+end
+
+function ZDFmediathek.table_append(source, dest)
+ for k,v in pairs(source) do
+ if type(v) == "table" then
+ if type(dest[k]) ~= "table" then
+ dest[k] = {}
+ end
+ ZDFmediathek.table_append(v, dest[k])
+ else
+ dest[k] = v
+ end
+ end
+end
+
+function ZDFmediathek.merge_stream_descriptor(s1, s2)
+ local t = {}
+ ZDFmediathek.table_append(s1, t)
+ ZDFmediathek.table_append(s2, t)
+ return t
+end
+
+function dump(o)
+if type(o) == 'table' then
+local s = '{ '
+for k,v in pairs(o) do
+if type(k) ~= 'number' then k = '"'..k..'"' end
+s = s .. '['..k..'] = ' .. dump(v) .. ','
+end
+return s .. '} '
+else
+return tostring(o)
+end
+end
+
+-- vim: set ts=2 sw=2 tw=72 expandtab: