Project

General

Profile

Files » vdr-streamdev-client-0.3.5.lua

mwa, 08/05/2017 10:55 AM

 
-- Copyright 2017 Martin Wache
-- VDR Streamdev Client is free software; you can redistribute it and/or
-- modify it under the terms of the GNU General Public License as published
-- by the Free Software Foundation Version 2 of the License.
-- You can obtain a copy of the License from
-- https://www.gnu.org/licenses/gpl2.html
--
-- VDR Streamdev Client
-- Version 0.3.4
--
-- A script which turns mpv into a client for VDR with the Streamdev-Plugin
--
-- Features:
-- * runs on Windows, Linux and Mac Os. (needs bash and netcat installed)
-- * easy channel switching a la vdr (with channel group support)
-- * show current and next epg event if available
-- * create timers from epg, disable/enable and remove timers
-- * watch recordings
--
-- Short instructions:
-- 1. Enable the streamdev-server-plugin in vdr
-- 2. Modify streamdevhosts.conf to contain the clients IP
-- 3. If you want to have channel names and epg info,
-- modify svdrphosts.conf to contain the clients IP.
-- Also netcat ('nc') and bash needs to be installed and in the path.
-- 4. Place this file in one of mpvs script folders
-- ( ~/.config/mpv/scripts/) or call mpv with the --script
-- command line option.
-- For now --script vdr-streamdev-client.lua is prefered.
-- 5. start mpv
-- mpv vdrstream://[vdr-host][:streamdev-port][/channel] [--script vdr-streamdev-client.lua]
--
-- When mpv is running in Streamdev client mode, you can use
-- the keys UP,DOWN, 0-9 to select channels.
-- ENTER will bring up the channel info display.
-- The key 'm' will show the menu.
--
-- Many thanks to:
-- - wolfi.m@vdr-portal.de for fixing the dimensions of the time-box
-- in the channel-info, the progress bar position and pointing
-- out that I forgot to remove my startup channel.
-- - jrie@vdr-portal.de for fixing the bash path for windows
--

local config = {
host="192.168.55.4",
svdrp_port="6419",
--svdrp_port="2004",
streamdev_port="3000",
-- default startup channel to show
startup_channel=1,
-- time after which the '0' key returns to this channel
previous_channel_time=10,
-- for how long to show playback/channel info
show_info_timeout=7,
-- timeout after which channel entry is assumed to be finished
channel_switch_timeout=5,

--media_dir="/Users/wache/Downloads/mps/",
media_dir="/Volumes/video",
-- all media extensions in upper case please!
media_extensions={".AVI",".MPG",".OGG",".M4A",".M3U",".MP4",".WEBM",},

-- if you don't want to use streamdev-streaming for recordings
-- you can provide the path to VDRs video directory here (mounted locally)
vdr_video_dir="",

-- recording MB/minute to estimate remaining recording time
mb_per_minute=25,

-- how often the current/next epg events are loaded from the server.
-- In seconds
epg_nownext_update_time=300,
-- time after which a schedule for a channel is considered out of date
-- and updated from the server. In seconds.
epg_channel_update_time=3600,
-- how long after the event ended it is still shown. In seconds.
epg_old_events_linger_time=120,

-- how much time before an event the timer starts. In minutes.
timer_margin_start=5,
-- how much time after an event the timer stops. In minutes.
timer_margin_stop=10,
-- the default lifetime of a recording (see VDRs manual)
timer_default_lifetime=99,
-- the default priority of a recording (see VDRs manual)
timer_default_priority=50,

osd_font_pixel_per_char=8,

-- osd colors
osd_background_color="000000",
osd_background_alpha="70",
osd_header_color="000090",
osd_header_alpha="70",
osd_highlight_color="000090",
osd_highlight_alpha="70",
osd_progressbar_fg_color="00F000",
osd_progressbar_fg_alpha="10",
osd_progressbar_bg_color="000090",
osd_progressbar_bg_alpha="10",
osd_red="0000F0",
osd_reda="70",
osd_green="00F000",
osd_greena="70",
osd_yellow="00F0F0",
osd_yellowa="70",
osd_blue="F00000",
osd_bluea="70",
osd_message_color="007700",
osd_message_alpha="10",
osd_confirm_color="007777",
osd_confirm_alpha="10",

osd_top_menu=20,
osd_left_menu=20,
osd_width_menu=430,
osd_height_menu=242,
osd_menu_max_items=9,
osd_menu_item_height=20,
osd_max_rows = 14,

osd_message_left=20,
osd_message_top=230,
osd_message_width=430,
osd_message_height=20,

osd_info_left=20,
osd_info_top=180,
osd_info_width=430,
osd_info_height=80,
}
require 'mp.options'
read_options(config,'vdr-streamdev-client')
local assdraw = require "mp.assdraw"

local channels = { }
local chno_to_idx = {}
local chid_to_idx = {}
local startup = 1
local has_svdrp = 0
local vdruri
local utils = require 'mp.utils'
local channel_idx=nil
local next_channel=0
local channel_timer
local last_channel=1
local next_last_channel=1
local update_last_channel_timeout
local disk_space_available=nil
local disk_space_free=nil
local disk_space_percent=nil
local epginfo = {}
local epginfo_time = {}
local epg_timer -- refreshes the epg info regulary
local timerinfo = {}

local vw=495
local vh=275

-- ************************* misc ***********************

function ends_with(str,str_end)
local str_len=str:len()
return str:sub(1+str:len()-str_end:len(),str_len)==str_end
end

function strip_end(str,str_end)
local str_len=str:len()
return str:sub(1,str:len()-str_end:len()+1)
end

function toArray(i)
local array={}
for v in i do
array[#array+1]=v
end
return array
end

function slice(tbl,first,last)
local s={}
for i = first or 1, last or #tbl do
s[#s+1] = tbl[i]
end
return s
end

-- ************************* state machine stuff **********************
local state={}
local state_livetv
local state_playback
local state_channel_info
local main_menu_items

function update_state()
local cstate=curr_state()
if (cstate.update_state) then
cstate:update_state()
end
end

function update_osd()
local cstate=curr_state()
mp.log("info","update_osd "..cstate.name)
local ass
if (cstate.update_osd) then
ass = cstate:update_osd()
else
ass = assdraw.ass_new()
end
if cstate.message ~= nil or cstate.confirm ~= nil then
-- message box
ass:new_event()
ass:pos(config.osd_message_left, config.osd_message_top)
if cstate.confirm ~= nil then
ass_color(ass,config.osd_confirm_color)
ass_alpha(ass,config.osd_confirm_alpha)
else
ass_color(ass,config.osd_message_color)
ass_alpha(ass,config.osd_message_alpha)
end
ass:draw_start()
ass:rect_ccw(0,0,config.osd_message_width,config.osd_message_height)
ass:draw_stop()

-- print message
ass:new_event()
ass:pos(config.osd_message_left, config.osd_message_top)
if cstate.confirm ~= nil then
ass:append(cstate.confirm)
else
ass:append(cstate.message)
end
end

mp.set_osd_ass(0, 0, ass.text)
end

function state_update_timeout()
local cstate=curr_state()
if cstate.timeout then
if cstate.timer == nil then
cstate.timer=mp.add_timeout(cstate.timeout,state_back)
else
cstate.timer:kill()
cstate.timer:resume()
end
end
if cstate.update_osd_timeout then
if cstate.osd_timer == nil then
cstate.osd_timer=mp.add_periodic_timer(cstate.update_osd_timeout,
function()
update_state()
update_osd()
end)
else
cstate.osd_timer:kill()
cstate.osd_timer:resume()
end
end
end

function state_remove_timeouts()
local cstate=curr_state()
if cstate.timer then
cstate.timer:kill()
end
if cstate.osd_timer then
cstate.osd_timer:kill()
end
end

function update_hide_osd_timeout()
local cstate=curr_state()
if cstate.hide_osd_timeout then
if cstate.hide_osd_timer ~= nil then
cstate.hide_osd_timer:kill()
end
cstate.hide_osd_timer=mp.add_timeout(cstate.hide_osd_timeout,
function()
cstate.update_osd=nil
update_osd()
end)
end
cstate.hide_osd_timeout=nil
end

function new_state(nstate)
table.insert(state,nstate)
mp.log("info","state_new "..nstate.name)
state_update_timeout()
update_state()
update_hide_osd_timeout()
update_osd()
end

function state_remove_including(name)
local cstate=curr_state()
while #state>1 and name ~= cstate.name do
state_back()
cstate=curr_state()
end
-- remove the state the name
state_back()
end

function state_back_to(name)
local cstate=curr_state()
while #state>1 and name ~= cstate.name do
state_back()
cstate=curr_state()
end
end

function state_back()
local cstate=curr_state()
state_remove_timeouts()
if #state>1 then
table.remove(state)
end
cstate=curr_state()
mp.log("info","state_back, new "..cstate.name)
update_state()
update_osd()
end

function curr_state()
return state[#state]
end

-- *********************** OSD stuff *******************************

function ass_color(ass,bgr)
ass:append("{\\1c&H"..bgr.."&}")
end
function ass_bdcolor(ass,bgr)
ass:append("{\\3c&H"..bgr.."&}")
end
function ass_alpha(ass,alpha)
ass:append("{\\1a&H"..alpha.."}")
end
function ass_bdalpha(ass,alpha)
ass:append("{\\3a&H"..alpha.."}")
end
function ass_scale_font(ass,scale)
ass:append("{\\fscx"..scale.."\\fscy"..scale.."}")
end
function ass_clip(ass,x1,y1,x2,y2)
ass:append("{\\clip("..x1..","..y1..","..x2..","..y2.."}")
end

local function vdrtime2str(t)
return t:sub(1,2)..":"..t:sub(3,5)
end

local function print_time(t)
if (t == nil) then
return " "
end
return os.date('%H:%M',t)
end

local function print_date(t)
if (t == nil) then
return " "
end
return os.date('%a %d.%m',t)
end

-- local fake time stamp
local function lts(options)
if options.hour == nil then options.hour=0 end
if options.min == nil then options.min=0 end
return ((((options.year-1970)*366+options.month)*31+options.day)*24+
options.hour)*60+options.min
end

local function to_lts(date, time)
local year=tonumber(date:sub(1,4))
local month=tonumber(date:sub(6,7))
local day=tonumber(date:sub(9,10))
local min=tonumber(time:sub(3,4))
local hour=tonumber(time:sub(1,2))
--return os.time{year=year,month=month,day=day,hour=hour,minute=min}
return lts{year=year,month=month,day=day,
hour=hour,min=min}
end

local function format_epg(epg_info)
if epg_info == nil then return "" end

local msg
msg=print_time(epg_info['start'])
if (epg_info.timer_status =="T") then
msg= msg.." REC: "
end
if (epg_info['title'] ~= nil) then
msg = msg.." "..epg_info['title']
end
return msg
end

local function date_format_epg(epg_info)
local msg=format_epg(epg_info)
msg = print_date(epg_info.start) .. " " .. msg
return msg
end

local function print_duration(t)
if t==nil then
return "xx:xx:xx"
end
local h=math.floor(t/3600)
local m=math.floor(t%3600/60)
local s=t%60
return string.format("%02d:%02d:%02d",h,m,s)
end

function curr_channel_id()
local cinfo = channels[channel_idx]
return cinfo and cinfo['id'] or nil
end

function channel_id_to_idx(cid)
return chid_to_idx[cid]
end

function channel_info_from_cid(cid)
return channels[chid_to_idx[cid]]
end

function draw_progressbar(ass,left,top, width, height, part)
ass:new_event()
ass:pos(left, top)
ass_color(ass,config.osd_progressbar_bg_color)
ass_alpha(ass,config.osd_progressbar_bg_alpha)
ass_bdcolor(ass,config.osd_progressbar_bg_color)
ass_bdalpha(ass,config.osd_progressbar_bg_alpha)
ass:draw_start()
ass:rect_ccw(0,0,width,height)
ass:draw_stop()
ass:new_event()

if part <0 then return end
ass:pos(left, top)
ass_color(ass,config.osd_progressbar_fg_color)
ass_alpha(ass,config.osd_progressbar_fg_alpha)
ass_bdcolor(ass,config.osd_progressbar_fg_color)
ass_bdalpha(ass,config.osd_progressbar_fg_alpha)
ass:draw_start()
ass:rect_ccw(0,0,part*width,height)
ass:draw_stop()
ass:new_event()
end

function show_playback_info(self)
--local time_pos = mp.get_property_native("time-pos")
local time_pos = mp.get_property_native("playback-time")
local max_time = mp.get_property_native("duration")
local ass = assdraw.ass_new()

-- channel info box
ass:new_event()
ass:pos(config.osd_info_left, config.osd_info_top)
ass_color(ass,config.osd_background_color)
ass_alpha(ass,config.osd_background_alpha)
ass:draw_start()
ass:rect_ccw(0,0,config.osd_info_width,config.osd_info_height)
ass:draw_stop()

-- print current playback time
ass:new_event()
ass:pos(config.osd_info_left, config.osd_info_top)
ass:append(print_duration(time_pos))

-- print playback length
ass:new_event()
ass:pos(config.osd_info_width-80, config.osd_info_top)
ass:append(print_duration(max_time))

-- print recording name
ass:new_event()
ass:pos(config.osd_info_left+80, config.osd_info_top)
ass_clip(ass,config.osd_info_left+80,config.osd_info_top,
config.osd_info_left+config.osd_info_width-100,config.osd_info_top+20)
ass_scale_font(ass,80)
ass:append(self.rinfo['name'])

if time_pos~= nil and max_time~= nil then
draw_progressbar(ass,config.osd_info_left+10,config.osd_info_top+30,
config.osd_info_width-config.osd_info_left-10,20,time_pos/max_time)
end

return ass
end

function show_channel_info(self)
local ass = assdraw.ass_new()
local cidx = next_channel==0 and channel_idx or chno_to_idx[next_channel]
local cinfo = cidx and channels[cidx] or nil
local chno = cinfo and cinfo.no or cidx
if next_channel ~= 0 then
-- channel switching by entering a number
chno = next_channel
cidx = chno_to_idx[chno]
cinfo = cidx and channels[cidx] or nil
else
cidx = channel_idx
cinfo = channels[cidx]
chno = cinfo and cinfo.no or cidx
end

-- channel info box
ass:pos(config.osd_info_left, config.osd_info_top)
ass_color(ass,config.osd_background_color)
ass_alpha(ass,config.osd_background_alpha)
ass:draw_start()
ass:rect_ccw(0,0,config.osd_info_width,config.osd_info_height)
ass:draw_stop()

-- info time box
ass:new_event()
ass:pos(config.osd_info_left, config.osd_info_top)
ass_color(ass,config.osd_header_color)
ass_alpha(ass,config.osd_header_alpha)
ass:draw_start()
ass:rect_ccw(0,0,55,23)
ass:draw_stop()

-- print time
ass:new_event()
ass:pos(config.osd_info_left, config.osd_info_top)
ass:append(os.date("%H:%M"))

-- channel name
ass:new_event()
ass:pos(config.osd_info_left+70, config.osd_info_top)
if cinfo and not cinfo.is_group_separator then
ass:append(chno)
if next_channel~=0 then
ass:append("_")
end
end
if cinfo then ass:append(" "..tostring(cinfo['name'])) end
ass:new_event()

local cid = cinfo and cinfo['id'] or nil
local einfo=get_epgnow(cid)
if einfo then
-- epg progress bar
local dwidth=200
local dheight=5
if einfo['start'] ~= nil and einfo['duration'] ~= nil then
local part = (os.time()-tonumber(einfo['start']))/
tonumber(einfo['duration'])
draw_progressbar(ass,config.osd_info_left+1, config.osd_info_top+24,
dwidth,dheight,part)
end

-- epg now info
ass:pos(config.osd_info_left+2, config.osd_info_top+35)
ass_clip(ass,config.osd_info_left,config.osd_info_top+35,
config.osd_info_left+config.osd_info_width,
config.osd_info_top+55)
ass_scale_font(ass,80)
ass:append(format_epg(einfo))
ass:new_event()
end

einfo = get_epgnext(cid)
if einfo ~= nil then
-- epg next info
ass:pos(config.osd_info_left+2, config.osd_info_top+55)
ass_clip(ass,config.osd_info_left,config.osd_info_top+55,
config.osd_info_left+config.osd_info_width,
config.osd_info_top+75)
ass_scale_font(ass,80)
ass:append(format_epg(einfo))
ass:new_event()
end

return ass
end

function create_menu_base(options)
local ass = assdraw.ass_new()
local bg = config.osd_background_color
local bga = config.osd_background_alpha
local hd = config.osd_header_color
local hda = config.osd_header_alpha
local red = config.osd_red
local reda = config.osd_reda
local green = config.osd_green
local greena = config.osd_greena
local yellow = config.osd_yellow
local yellowa = config.osd_yellowa
local blue = config.osd_blue
local bluea = config.osd_bluea

-- menu box
ass:pos(config.osd_left_menu, config.osd_top_menu)
ass_color(ass,bg)
ass_alpha(ass,bga)
ass:draw_start()
ass:rect_ccw(0,0,config.osd_width_menu,config.osd_height_menu)
ass:draw_stop()
ass:new_event()

-- header box
ass:pos(config.osd_left_menu, config.osd_top_menu)
ass_color(ass,hd)
ass_alpha(ass,hda)
ass:draw_start()
ass:rect_ccw(0,0,config.osd_width_menu,20)
ass:draw_stop()
ass:new_event()

-- header
ass:pos(config.osd_left_menu, config.osd_top_menu)
ass:append(os.date("%H:%M"))
if options and options.name then ass:append(" "..options.name) end
ass:new_event()

-- footer
ass:pos(config.osd_left_menu,config.osd_top_menu+config.osd_height_menu-20)
ass_color(ass,red)
ass_alpha(ass,reda)
ass:draw_start()
ass:rect_ccw(0,0,config.osd_width_menu/4,20)
ass:draw_stop()
ass:new_event()
if options and options.red then
ass:pos(config.osd_left_menu+2,config.osd_top_menu+config.osd_height_menu-20)
ass_scale_font(ass,80)
ass:append(options.red)
ass:new_event()
end

ass:pos(config.osd_left_menu+config.osd_width_menu/4,config.osd_top_menu+config.osd_height_menu-20)
ass_color(ass,green)
ass_alpha(ass,greena)
ass:draw_start()
ass:rect_ccw(0,0,config.osd_width_menu/4,20)
ass:draw_stop()
ass:new_event()
if options and options.green then
ass:pos(config.osd_left_menu+config.osd_width_menu/4+2,config.osd_top_menu+config.osd_height_menu-20)
ass_scale_font(ass,80)
ass:append(options.green)
ass:new_event()
end

ass:pos(config.osd_left_menu+config.osd_width_menu/4*2,config.osd_top_menu+config.osd_height_menu-20)
ass_color(ass,yellow)
ass_alpha(ass,yellowa)
ass:draw_start()
ass:rect_ccw(0,0,config.osd_width_menu/4,20)
ass:draw_stop()
ass:new_event()
if options and options.yellow then
ass:pos(config.osd_left_menu+config.osd_width_menu/4*2+2,config.osd_top_menu+config.osd_height_menu-20)
ass_scale_font(ass,80)
ass:append(options.yellow)
ass:new_event()
end

ass:pos(config.osd_left_menu+config.osd_width_menu/4*3,config.osd_top_menu+config.osd_height_menu-20)
ass_color(ass,blue)
ass_alpha(ass,bluea)
ass:draw_start()
ass:rect_ccw(0,0,config.osd_width_menu/4,20)
ass:draw_stop()
ass:new_event()
if options and options.blue then
ass:pos(config.osd_left_menu+config.osd_width_menu/4*3+2,config.osd_top_menu+config.osd_height_menu-20)
ass_scale_font(ass,80)
ass:append(options.blue)
ass:new_event()
end

return ass
end

function draw_scrollbar(ass,left,top,width,height,pos,max)
local part=height/max
local bheight=part
if part<10 then bheight=10 end
if pos<0 or max < 1 then return end
part = (pos-1)*(height-bheight)/max
ass:pos(left,top)
ass_color(ass,config.osd_highlight_color)
ass_alpha(ass,config.osd_highlight_alpha)
ass:draw_start()
ass:rect_ccw(0,part,width,part+bheight)
ass:draw_stop()
ass:new_event()
end

local item_w=config.osd_width_menu - 11
function show_menu(self)
local ass = create_menu_base{name=self.header,
red=self.red_name,green=self.green_name,
yellow=self.yellow_name,blue=self.blue_name}
if self.selected_item == nil then self.selected_item = 1 end
if self.items == nil or #self.items == 0 then
return ass
end
if self.start_pos == nil or self.start_pos < 1 then self.start_pos = 1 end
if self.selected_item>self.start_pos+config.osd_menu_max_items then
self.start_pos=self.selected_item - config.osd_menu_max_items
end
if self.selected_item<self.start_pos then
self.start_pos=self.selected_item
end
local maxi = #self.items>self.start_pos+config.osd_menu_max_items
and self.start_pos+config.osd_menu_max_items or #self.items
local draw_item=self.draw_item and self.draw_item or
draw_column_item(nil,self.column_width)
for i = self.start_pos,maxi do
local v = self.items[i]
local itop= config.osd_top_menu+1+(i-self.start_pos+1)*config.osd_menu_item_height
if self.selected_item == i then
ass:pos(config.osd_left_menu, itop)
ass_color(ass,config.osd_highlight_color)
ass_alpha(ass,config.osd_highlight_alpha)
ass:draw_start()
ass:rect_ccw(0,0,item_w,config.osd_menu_item_height)
ass:draw_stop()
ass:new_event()
end
if v.draw == nil then
draw_item(v,ass,config.osd_left_menu+2,itop,item_w,config.osd_menu_item_height)
else
v:draw(ass,config.osd_left_menu+2,itop,item_w,config.osd_menu_item_height)
end
ass:new_event()
end
if #self.items>config.osd_menu_max_items then
draw_scrollbar(ass,config.osd_left_menu+config.osd_width_menu-10,
config.osd_top_menu+21,9,config.osd_height_menu-42,
self.start_pos,#self.items-config.osd_menu_max_items)
end

return ass
end

function menu_handle_key(self,k)
if k=="UP" then
self.selected_item = self.selected_item - 1
elseif k=="DOWN" then
self.selected_item = self.selected_item + 1
elseif k=="LEFT" then
self.selected_item = self.selected_item - config.osd_menu_max_items
elseif k=="RIGHT" then
self.selected_item = self.selected_item + config.osd_menu_max_items
elseif k=="BS" then
state_back()
elseif k=="RED" and self.red_action then
self:red_action()
elseif k=="GREEN" and self.green_action then
self:green_action()
elseif k=="YELLOW" and self.yellow_action then
self:yellow_action()
elseif k=="BLUE" and self.blue_action then
self:blue_action()
elseif k=="ENTER" then
local item = self.items[self.selected_item]
if item and item.action then
item:action()
end
elseif k=="MENU" then
state_remove_including("main_menu")
elseif type(k) == "number" and k>=0 and k<=9 then
self.selected_item = k
end

if self.selected_item then
if self.items ~= nil and self.selected_item > #self.items then
self.selected_item = #self.items
end
if self.selected_item < 1 then
self.selected_item = 1
end
end
update_osd()
end

local margin=20
function split_text(max_len,text)
local pos = 1
return function()
local npos = text:find("\n",pos)
if npos == nil then npos=text:len()+1 end

if npos-pos>max_len then
-- find space to split the string
npos=text:find("[ -.+]",pos+max_len-margin>0 and pos+max_len-margin or 0)
if npos == nil then npos=text:len()+1 end
end

if pos>=text:len() then
return nil
end
local ret = text:sub(pos,npos)
pos = npos + 1
return ret
end
end

-- shows text in self.text
-- uses self.header, self.title, self.subtitle
function show_text(self)
local ass = create_menu_base{name=self.header,
red=self.red_name,green=self.green_name,
yellow=self.yellow_name,blue=self.blue_name}
local i = 0
local t = config.osd_top_menu + 23
local l = config.osd_left_menu + 2
local max_rows=config.osd_max_rows
local text = self.text and toArray(split_text(config.osd_width_menu/config.osd_font_pixel_per_char/0.8,self.text)) or {}


if self.title then
-- time, title
ass:pos(l, t)
ass_clip(ass,l,t,l+config.osd_width_menu-10,t+25)
ass:append(tostring(self.title))
ass:new_event()
t = t + 25
max_rows = max_rows - 2
end
-- subtitle
if self.subtitle then
ass:pos(l, t)
ass_clip(ass,l,t,l+config.osd_width_menu-10,t+20)
ass_scale_font(ass,80)
ass:append(tostring(self.subtitle))
ass:new_event()
t = t + 20
max_rows = max_rows - 2
end
if self.start_pos == nil then self.start_pos = 1 end
if self.start_pos > #text-max_rows then self.start_pos = #text-max_rows end
if self.start_pos < 1 then self.start_pos = 1 end
local sp=self.start_pos
if self.text then
for j=0,max_rows do
local v = text[j+sp]
if v then
ass:pos(l, t + j*13)
ass_scale_font(ass,70)
ass:append(v)
ass:new_event()
end
end
end
if #text > max_rows+1 then
draw_scrollbar(ass,config.osd_left_menu+config.osd_width_menu-10,
config.osd_top_menu+21,9,config.osd_height_menu-42,
self.start_pos,#text-max_rows-1)
end

return ass
end

function key(k)
return function()
local cstate=curr_state()
mp.log("info","state name "..cstate.name)
mp.log("info","key "..k)
if cstate.confirm ~= nil then
local action=cstate.confirm_action
cstate.confirm=nil
cstate.confirm_action=nil
if k=="ENTER" and action ~= nil then
action(cstate)
end
update_osd()
return
end
if cstate and cstate.handle_key then
cstate:handle_key(k)
end
end
end

local function send_webrequest(path)
ret = utils.subprocess({
args= {'bash', '-c', '( printf "GET /'..path
..' HTTP/1.0\n\n"; sleep 1)|nc '..config.host..' '
..config.streamdev_port},
-- args= {'/bin/bash', '-c', 'echo "'..command
-- ..'" >/dev/tcp/'..config.host..'/'..config.svdrp_port},
cancellable=false,
})
return ret.stdout
end

local function parse_ext3mu(stdout)
mp.log("info","Parsing ext3mu")
local state='http_header'
local ret={}
local title={}
for l in string.gmatch(stdout,"[^\r\n]+") do
if state=='http_header' then
if l~="HTTP/1.0 200 OK" then
state="error_http_header"
break
end
state="http_header_content"
elseif state=="http_header_content" then
if l~="Content-type: audio/x-mpegurl; charset=UTF-8" then
state="error_http_content"
break
end
state="content_header"
elseif state=="content_header" then
if l~="#EXTM3U" then
state="error_content_header"
break
end
state="content_line_header"
elseif state=="content_line_header" then
if l:sub(1,11)~="#EXTINF:-1," then
state="error_content_line_header"
mp.log("info","'"..l:sub(1,11).."'")
break
end
local info=toArray(string.gmatch(l:sub(12),"[^ ]+"))
title['idx']=info[1]
title['day']=info[2]
title['time']=info[3]
title['name']=table.concat(slice(info,4)," ")
state = "content_line"
elseif state=="content_line" then
title['url']=l
table.insert(ret,title)
title = {}
state = "content_line_header"
end
end
mp.log("info",state)
return ret
end

local function get_info_svdrp(self)
mp.log("info","get_info_svdrp "..tostring(self.name))
local info=parse_lste(send_svdrp(string.format("LSTR %d",self.idx)))
for i,v in pairs(info) do
mp.log("info",tostring(i)..":"..tostring(v))
for j,event in pairs(v) do
mp.log("info",tostring(j)..":"..tostring(event))
return event.description,format_epg(event),event.subtitle
end
end
end

local function parse_lstr(stdout)
mp.log("info","Parsing lstr")
local ret={}
for i in string.gmatch(stdout,"[^\r\n]+") do
local code = i:sub(1,3)
if code == "250" then
local c = i:sub(5,5)
local line = i:sub(5)
local idx,date,time,length,new,name = line:match("(%d+) (%d%d.%d%d.%d%d) (%d%d:%d%d) (%d?%d:%d%d)(%*?) +(.*)")
local title={
idx=idx,
day=date,
time=time,
length=length,
new=new,
name=name,
url=string.format("http://%s:%d/%d.rec.ts",config.host,config.streamdev_port,idx),
info=get_info_svdrp,
}
table.insert(ret,title)
else
mp.log("info","parse_lstr unknown "..i)
end
end
return ret
end

local function collect_directories(recordings)
mp.log("info","collect recordings")
local ret={}
local cache_tree={
entries={},
child={},
}
for i,v in pairs(recordings) do
local spath=toArray(string.gmatch(v['name'],"[^~]+"))
local j=1
local dir=ret
local cache=cache_tree
while (j~=#spath) do
local name=spath[j]
if cache.child[name] == nil then
cache.child[name] = {
entries={},
child={},
}
cache.entries[name]={
name=name,
title=name,
}
table.insert(dir,cache.entries[name])
end
dir=cache.entries[name]
cache=cache.child[name]
j = j + 1
end
v.title=spath[#spath]
table.insert(dir,v)
end
return ret
end

function send_svdrp(command)
ret = utils.subprocess({
args= {'bash', '-c', '(printf "'..command
..'\n" ; sleep 0.1)|nc '..config.host..' '..config.svdrp_port},
-- args= {'/bin/bash', '-c', 'echo "'..command
-- ..'" >/dev/tcp/'..config.host..'/'..config.svdrp_port},
cancellable=false,
})
if ret.error=="init" then
mp.log("warn","Could not contact VDR server. Do you have 'bash' and 'nc' (netcat) installed?")
end
if ret.status ~= 0 then
mp.log("warn","Could not contact VDR server. Is the SVDRP port "..config.svdrp_port.." correct, is it open and the client's IP in svdrphosts.conf?")
end
return ret.stdout
end

function check_for_errors(stdout)
for i in string.gmatch(stdout,"[^\r\n]+") do
local code = i:sub(1,3)
if code:sub(1,1) == "5" then
return i:sub(5)
end
end
end

local function parse_lstc(stdout)
mp.log("info","Getting channel list")
for i in string.gmatch(stdout,"[^\r\n]+") do
local code = i:sub(1,3)
local line = i:sub(5)
if (code ~= "250") then
mp.log("info","Unknown code '"..i.."'")
else
local channel_end=i:find(";")
if (channel_end ==nil) then
if i:sub(5,5)=="0" then
-- channel separator
local cinfo={}
local start_cno,groupname=i:match("0 :@?(%d*) ?(.*)")
cinfo.is_group_separator=true
cinfo.name = groupname
mp.log("info","found group "..utils.to_string(cinfo))
table.insert(channels,cinfo)
end
else
-- normal channel
local channel=i:sub(5,channel_end-1)
local sp=channel:find(" ")
if (sp ~= nil) then
local c =tonumber(channel:sub(0,sp-1))
local cinfo={}
cinfo['no']=c
cinfo['name']=channel:sub(sp+1)
-- S19.2E-1-1079-28011 ZDFinfo (S19.2E)
-- ZDFinfo;ZDFvision:11953:HC34M2S0:S19.2E:27500:610=2:620=deu@3,621=mis@3,622=mul@3;625=deu@106:630;631=deu:0:28011:1:1079:0
local para=toArray(i:sub(channel_end+1):gmatch("[^:]+"))
if (#para>12) then
local cid=para[4].."-"..para[11].."-"..para[12].."-"..para[10]
--mp.log("info",cid)
cinfo['id']=cid
else
mp.log("info","para > 12: "..tostring(#para))
end
table.insert(channels,cinfo)
cinfo.idx=#channels
chno_to_idx[cinfo.no]=#channels
if cinfo.id then
chid_to_idx[cinfo.id]=#channels
else
mp.log("info","no channel id "..channel)
end
end
end
end
end
end

local function get_channels()
parse_lstc(send_svdrp('LSTC :groups'))
if #channels==0 then
mp.log("warn","Could not load channel list, only basic functionality will be available.")
else
has_svdrp=1
end
end

local function parse_plug(stdout)
for i in string.gmatch(stdout,"[^\r\n]+") do
local code = i:sub(1,3)
if code == "214" then
local line = i:sub(5)
if line == "Available plugins:" then
elseif line == "End of plugin list" then
else
local plugin=line:sub(1,line:find(" ")-1)
mp.log("info","found plugin "..tostring(plugin))
if plugin == "svdrposd" then
table.insert( main_menu_items,#main_menu_items+1,{
text="Server OSD",
action = function()
new_state( create_remote_osd_menu_state() )
end,
})
end
end
end
end
end

function check_for_plugins()
parse_plug(send_svdrp("PLUG"))
end

-- ************************* epg stuff ***********************

local function parse_stat(line)
disk_space_available,disk_space_free,disk_space_percent=line:match("(%d+)MB (%d+)MB (%d+)%%")
end

function update_state_disk_header(self)
if disk_space_available ~= nil then
local free_m=math.floor(disk_space_free/config.mb_per_minute)
self.header=string.format("Disk %d%% Free %02d:%02dh",
disk_space_percent, math.floor(free_m/60), free_m%60)
end
end

local function parse_event(event,line)
--mp.log("info","prase_event "..tostring(line))
if event==nil then event={} end
local code = line:sub(1,2)
local value = line:sub(3)
if code == "C " then
event.cid=value:sub(1,value:find(" ")-1)
elseif code == "E " then
local p=toArray(value:gmatch("[^ ]+"))
event['start'] = tonumber(p[2])
event['duration'] = tonumber(p[3])
event['stop'] = event.start + event.duration
event.start_date_str=print_date(event.start)
event.start_time_str=print_time(event.start)
event.start_lts=lts(os.date("*t",event.start))
event.stop_lts=lts(os.date("*t",event.stop))
event.cid=cid
elseif code == "T " then
event['title']=value
elseif code == "S " then
event['subtitle']=value
elseif code == "D " then
event['description']=value
else
--mp.log("info","Unknwon code "..tostring(code))
end
return event
end

function parse_lste(stdout)
local epginfo={}
local cid
local this_event = {}
local this_channel
mp.log("info","Updating epg")
for i in string.gmatch(stdout,"[^\r\n]+") do
local code = i:sub(1,3)
if code == "215" then
local c = i:sub(5,5)
local line = i:sub(5)
if c == "C" then
-- new channel
this_event=parse_event({},line)
this_channel = {}
cid = this_event.cid
epginfo[cid]=this_channel
elseif c =="e" then
-- end of event
this_event = {}
elseif c =="c" then
-- end of channel
if #this_channel==0 then epginfo[cid]=nil end
--mp.log("info","insert "..tostring(cid).." "..#this_channel)
else
this_event = parse_event(this_event,line)
if this_channel[#this_channel]~=this_event and
this_event.start then
this_event.cid = cid
table.insert(this_channel,this_event)
end
end
elseif code == "250" then
-- the reply to the stat command we send with update epg command
local line = i:sub(5)
parse_stat(line)
end
end
return epginfo
end

local function merge_epg_channel(dst,src)
local di=1
local si=1
while dst[1]~=nil and dst[1].start+dst[1].duration
+config.epg_old_events_linger_time<os.time() do
--mp.log("info","removing old epg event "..tostring(dst[1].title))
table.remove(dst,1)
end
while si<=#src do
if dst[di]== nil or dst[di].start > src[si].start then
table.insert(dst,di,src[si])
di = di + 1
si = si + 1
elseif dst[di].start == src[si].start then
-- skip it it's already in
di = di + 1
si = si + 1
else
-- next destination item
di = di + 1
end
end
end

local function merge_epg(dst,src)
for cid,epg in pairs(src) do
if dst[cid] ~= nil then
merge_epg_channel(dst[cid],epg)
else
dst[cid]=epg
end
end
end

function get_epgnow(cid)
if epginfo[cid] == nil or epginfo[cid][1] == nil then
return nil
end
local dst=epginfo[cid]
while dst[1] and dst[1].start+dst[1].duration
+config.epg_old_events_linger_time<os.time() do
--mp.log("info","removing old epg event "..tostring(dst[1].title))
table.remove(dst,1)
end
return dst[1]
end

function get_epgnext(cid)
return epginfo[cid] and (epginfo[cid][2] and epginfo[cid][2] or nil) or nil
end

local function load_epg_now()
merge_epg(epginfo,parse_lste(send_svdrp("LSTE now")))
end

local function load_epg_next()
merge_epg(epginfo,parse_lste(send_svdrp("LSTE next\n STAT disk")))
end

local function load_epg_channel(cid)
mp.log("info","load_epg_channel "..tostring(cid))
if epginfo_time[cid] == nil or
epginfo_time[cid]+config.epg_channel_update_time < os.time() then
merge_epg(epginfo, parse_lste(send_svdrp("LSTE "..cid)))
epginfo_time[cid]=os.time()
end
match_timer_to_event(timerinfo[cid],epginfo[cid])
end

-- ************************* timer stuff ***********************

local function parse_lstt(stdout)
local timerinfo={}
mp.log("info","Updating timers")
for i in string.gmatch(stdout,"[^\r\n]+") do
local code = i:sub(1,3)
if (code == "250") then
local timer={}
local setting_pos=i:find(" ",5)
timer.id=tonumber(i:sub(5,setting_pos-1))
local t=toArray(i:sub(setting_pos+1):gmatch("[^:]+"))
timer.enabled=tonumber(t[1])
if timer.enabled==1 then
timer.enabled_str=">"
elseif timer.enabled==9 then
timer.enabled_str="#"
else
timer.enabled_str=""
end
timer.cid=t[2]
timer.chno=channel_info_from_cid(timer.cid).no
timer.day=t[3]
timer.day_str=timer.day:sub(9,10).."."..timer.day:sub(6,7)
timer.start=t[4]
timer.start_str=vdrtime2str(timer.start)
timer.start_lts=to_lts(timer.day,timer.start)
timer.stop=t[5]
timer.stop_str=vdrtime2str(timer.stop)
timer.stop_lts=to_lts(timer.day,timer.stop)
if timer.stop_lts<timer.start_lts then
timer.stop_lts=timer.stop_lts+24*60
end
timer.priority=t[6]
timer.lifetime=t[7]
timer.name=t[8]
timer.aux= t[9]~=nil and t[9] or ""
table.insert(timerinfo,timer)
end
end
return timerinfo
end

local function timer_from_event(event)
local timer={}
timer.id=nil
timer.enabled=1
timer.cid=event.cid
timer.chno=channel_info_from_cid(event.cid).no
timer.day=os.date("%Y-%m-%d",event.start-config.timer_margin_start*60)
timer.start=os.date("%H%M",event.start-config.timer_margin_start*60)
timer.stop=os.date("%H%M",event.start+event.duration+config.timer_margin_stop*60)
timer.priority=config.timer_default_priority
timer.lifetime=config.timer_default_lifetime
if event.title ~= nil and string.len(event.title)>1 then
timer.name=event.title
else
timer.name=os.date("Rec %Y-%M-%d %H:%m",event.start)
end
timer.aux=""
return timer
end

local function toggle_timer_onoff(timer)
if timer.enabled==0 then
timer.enabled=1
else
timer.enabled=0
end
end

local function send_update_timer(timer)
local timer_str=""
timer_str = timer_str..timer.enabled..":"
timer_str = timer_str..timer.cid..":"
timer_str = timer_str..timer.day..":"
timer_str = timer_str..timer.start..":"
timer_str = timer_str..timer.stop..":"
timer_str = timer_str..timer.priority..":"
timer_str = timer_str..timer.lifetime..":"
timer_str = timer_str..timer.name..":"
timer_str = timer_str..timer.aux
mp.log("info","send_update_timer: "..tostring(timer_str))
local result
if timer.id == nil then
result =send_svdrp("UPDT "..timer_str)
else
timer_str= tostring(timer.id).." "..timer_str
result =send_svdrp("MODT "..timer_str)
end
mp.log("info","send_update_timer: "..tostring(result))

return check_for_errors(result)
end

local function send_delete_timer(timer)
mp.log("info","send_delete_timer "..tostring(timer.id).." "..tostring(timer.name) )
local result=send_svdrp("DELT "..tostring(timer.id))
return check_for_errors(result)
end

function load_timers()
local timers=parse_lstt(send_svdrp("LSTT id"))
timerinfo={}
for i,t in pairs(timers) do
if timerinfo[t.cid] == nil then
timerinfo[t.cid] = {}
end
table.insert(timerinfo[t.cid],t)
end
for i,t in pairs(timerinfo) do
table.sort(t,function(a,b) return a.start_lts<b.start_lts end )
end
table.sort(timers,function(a,b) return a.start_lts<b.start_lts end)
return timers
end

function match_timer_to_event(timers,events, max_events)
local ei=1
if events == nil then events={} end
if max_events == nil then max_events=#events end
if max_events > #events then max_events=#events end
--mp.log("info","match_timer_to_event max_events "..max_events)
while ei<=max_events do
local ti=1
local event=events[ei]

event.timer_status=" "
event.timer=nil
--mp.log("info","event "..date_format_epg(event))
--mp.log("info","event "..event.start_lts.." "..event.stop_lts)
while timers and ti<=#timers and timers[ti].start_lts < event.stop_lts do
--mp.log("info","timer s tls "..timers[ti].start_lts.." stop: "..timers[ti].stop_lts.." "..timers[ti].day.." "..timers[ti].start.." "..timers[ti].stop..tostring(timers[ti].name))
if timers[ti].enabled == 0 then
-- ignore disabled timers
elseif timers[ti].stop_lts < event.start_lts then
-- do nothing, timer stops before this event starts
--mp.log("info","stops early "..timers[ti].stop_lts.." "..timers[ti].day.." "..timers[ti].start.." "..timers[ti].stop..tostring(timers[ti].name))
elseif timers[ti].start_lts < event.start_lts then
if timers[ti].stop_lts > event.stop_lts then
event.timer_status="T"
event.timer=timers[ti]
timers[ti].event=event
mp.log("info","found match "..tostring(event.title))
else
-- partial recording, missing end
if event.timer_status == " " then
event.timer_status="t"
mp.log("info","found p match missing end "..tostring(event.title))
--mp.log("info","timer stop "..timers[ti].stop_lts.." "..timers[ti].day.." "..timers[ti].start.." "..timers[ti].stop..tostring(timers[ti].name))
--mp.log("info","event "..date_format_epg(event))
--mp.log("info","event "..event.start_lts.." "..event.stop_lts)
end
end
else
if event.timer_status == " " then
-- partial recording, missing start
event.timer_status="t"
mp.log("info","found p match missing start "..tostring(event.title))
--mp.log("info","timer stop "..timers[ti].stop_lts.." "..timers[ti].day.." "..timers[ti].start.." "..timers[ti].stop..tostring(timers[ti].name))
--mp.log("info","event "..date_format_epg(event))
--mp.log("info","event "..event.start_lts.." "..event.stop_lts)
end
end
ti = ti + 1
end
ei = ei +1
end
end

local function match_nownext_timer_to_event()
local cid,events
for cid,events in pairs(epginfo) do
match_timer_to_event(timerinfo[cid],events,3)
end
end

-- ************************* remote osd ***********************

local function parse_svdrposd_lsto(stdout)
local osdinfo={
open=false,
items={},
}
mp.log("info","Updating remote osd")
for i in string.gmatch(stdout,"[^\r\n]+") do
local code=i:sub(1,3)
if code == "920" then
osdinfo.open=true
local t=i:sub(5,5)
if t=="T" then
osdinfo.title=i:sub(7)
elseif t=="S" then
table.insert(osdinfo.items, {text=i:sub(7)})
osdinfo.selected=#osdinfo.items
elseif t=="I" then
table.insert(osdinfo.items, {text=i:sub(7)})
elseif t=="X" then
osdinfo.text=i:sub(7)
elseif t=="R" then
osdinfo.red=i:sub(7)
elseif t=="G" then
osdinfo.green=i:sub(7)
elseif t=="Y" then
osdinfo.yellow=i:sub(7)
elseif t=="B" then
osdinfo.blue=i:sub(7)
else
mp.log("info","unknown osd code "..tostring(i))
end
elseif code == "930" then
osdinfo.open=false
else
mp.log("info","unknown osd code "..tostring(i))
end
end
return osdinfo
end

local function switch_channel(no)
mp.log("info","switch_channel "..no)
if tonumber(no) == nil then
no = channel_id_to_idx(no)
end
while #channels > 0 and channels[no] and channels[no].is_group_separator do
no = no + 1
end
local sav_channel=channel_idx
if update_last_channel_timeout ~= nil then
update_last_channel_timeout:kill()
end
update_last_channel_timeout=mp.add_timeout(config.previous_channel_time,
function()
mp.log("info","Updating last channel to "..tostring(sav_channel))
mp.log("info","last_channel :"..tostring(last_channel).." next_lc "..tostring(next_last_channel))
last_channel=next_last_channel
next_last_channel=sav_channel
mp.log("info","last_channel :"..tostring(last_channel).." next_lc "..tostring(next_last_channel))
end)
channel_idx=no
mp.set_property("demuxer-lavf-format","mpegts")
mp.set_property("keep-open","yes")
local cinfo = channels[channel_idx]
if cinfo and cinfo.id then
mp.commandv("loadfile",vdruri .. cinfo.id)
else
mp.commandv("loadfile",vdruri .. channel_idx)
end
if cinfo and cinfo.name then
mp.set_property("force-media-title",cinfo.name)
end
mp.log("info","speed "..tostring(mp.get_property("speed")))
mp.set_property("speed",0.9)
if speed_timer~=nil then
speed_timer:kill()
end
speed_timer=mp.add_timeout(20,function()
mp.log("info","resetting speed "..tostring(mp.get_property("speed")))
mp.set_property("speed",1)
mp.log("info","resetting speed "..tostring(mp.get_property("speed")))
end)

local state=curr_state()
if state.name =="livetv" then
state.update_osd = show_channel_info
state.hide_osd_timeout=config.show_info_timeout
update_hide_osd_timeout()
end
next_channel=0
update_osd()
end

local function playback_recording(rinfo)
local url = rinfo['url']
mp.log("info","play_rec "..tostring(url))
mp.commandv("playlist-clear")
--mp.commandv("playlist-remove","current")
if type(url)=="table" then
mp.log("info","loadfile "..tostring(url[1]))
mp.commandv("loadfile",url[1])
for i=2,#url do
mp.log("info","loadfile "..tostring(url[i]))
mp.commandv("loadfile",url[i],"append")
end
else
mp.commandv("loadfile",url)
end
if rinfo and rinfo.title then
mp.set_property("force-media-title",rinfo.title)
end
end

local function channel_next()
mp.log("info","next channel called " .. channel_idx .. " len channels " .. #channels)
channel_idx = channel_idx + 1
while #channels and channels[channel_idx].is_group_separator do
channel_idx = channel_idx + 1
end

if #channels>0 and channel_idx > #channels then
channel_idx = 1
end
switch_channel(channel_idx);
end
local function channel_prev()
mp.log("info","next channel called " .. channel_idx .. " len channels " .. #channels)
channel_idx = channel_idx - 1
while #channels and channels[channel_idx] and channels[channel_idx].is_group_separator do
channel_idx = channel_idx - 1
end
if (channel_idx < 1) then
channel_idx =#channels > 0 and #channels or 1
end
switch_channel(channel_idx);
end

local function next_group()
mp.log("info","next group called")
local sav = channel_idx
channel_idx = channel_idx + 1
while #channels and channels[channel_idx] and not channels[channel_idx].is_group_separator do
channel_idx = channel_idx + 1
end
if #channels and channel_idx>#channels then
channel_idx=sav
end
end

local function prev_group()
mp.log("info","next group called")
local sav = channel_idx
channel_idx = channel_idx - 1
while #channels and channels[channel_idx] and not channels[channel_idx].is_group_separator do
channel_idx = channel_idx - 1
end
if channel_idx < 1 then
channel_idx = sav
end
end


function update_channel_timer()
if ( channel_timer ~= nil ) then
channel_timer:kill()
end
channel_timer=mp.add_timeout(config.channel_switch_timeout, function()
if ( next_channel ~= 0 ) then
local ncid=chno_to_idx[next_channel]
if ncid then
switch_channel(ncid)
else
switch_channel(next_channel)
end
next_channel = 0
else
-- switch to current group
switch_channel(channel_idx)
end
end)
end

local function livetv_handle_key(self,key)
mp.log("info","state name "..self.name)
local show_osd=function()
self.update_osd = show_channel_info
self.hide_osd_timeout = config.show_info_timeout
update_hide_osd_timeout()
update_osd()
end
if key=="ENTER" then
if next_channel ~= 0 then
local ncid=chno_to_idx[next_channel]
if ncid then
switch_channel(ncid)
else
switch_channel(next_channel)
end
next_channel = 0
elseif channels[channel_idx] and channels[channel_idx].is_group_separator then
-- confirm group switch
channel_timer:kill()
switch_channel(channel_idx)
elseif self.update_osd == nil then
show_osd()
else
self.update_osd = nil
update_osd()
end
elseif key=="MENU" then
if has_svdrp==1 then
-- no menu without svdrp connection
state_main_menu.selected_item=1
new_state(state_main_menu)
end
elseif key=="UP" then
channel_next()
elseif key=="DOWN" then
channel_prev()
elseif key=="RIGHT" then
next_group()
update_channel_timer()
show_osd()
elseif key=="LEFT" then
prev_group()
update_channel_timer()
show_osd()
elseif type(key) =="number" then
if key == 0 and next_channel == 0 then
-- immediatly update last_channel
mp.log("info","last_channel :"..tostring(last_channel).." next_lc "..tostring(next_last_channel))
switch_channel(last_channel);
local sav_channel=last_channel
last_channel=next_last_channel
next_last_channel=sav_channel
mp.log("info","last_channel :"..tostring(last_channel).." next_lc "..tostring(next_last_channel))
return
end
next_channel=next_channel*10+key
self.update_osd=show_channel_info
update_osd()
update_channel_timer()
end
end

local function on_start()
local url = mp.get_property("stream-open-filename")

if (url:find("vdrstream://") == 1) then
if ( startup == 1) then
do_startup(url)
startup = 0
end
-- mp.set_property("stream-open-filename",channels[channel_idx])
--mp.set_property("cache-size",1024)
switch_channel(channel_idx)
--rinfo= {
--url="blah",
--name="Diese Datei",
--}
--new_state(create_playback_state(rinfo))
end
mp.log("info","Lua version " .. _VERSION)
end

function new_show_epgs_state(epg_info,channel_info)
epg_info = epg_info and epg_info or {description="No data"}
channel_info = channel_info and channel_info or {}
local state_show_epg = {
name = "menu_epg",
handle_key = function(self,k)
if k=="ENTER" or k=="BS" then
state_back()
elseif k=="DOWN" then
self.start_pos=self.start_pos+1
update_osd()
elseif k=="UP" then
self.start_pos=self.start_pos-1
update_osd()
elseif k=="RED" and self.red_action then
self:red_action()
elseif k=="GREEN" and self.green_action then
self:green_action()
elseif k=="YELLOW" and self.yellow_action then
self:yellow_action()
elseif k=="BLUE" and self.blue_action then
self:blue_action()
elseif k=="MENU" then
state_remove_including("main_menu")
elseif k=="BLUE" then
state_back_to("livetv")
switch_channel(self.cinfo['idx'])
end
end,

update_osd = show_text,
text = epg_info.description and epg_info.description:gsub('|','\n') or nil,
title = format_epg(epg_info),
subtitle = epg_info['subtitle'],
cinfo = channel_info,
red_name = 'Record',
}
state_show_epg.red_action = function()
action_record(state_show_epg,epg_info)
end
return state_show_epg
end

-- returns self[part1][part2]..[partn] for col_name 'part1.part2...partn'
function get_col(self,col_name)
local p = col_name:find("%.")
if p then
return get_col(self[col_name:sub(1,p-1)],col_name:sub(p+1))
end
return self[col_name]
end

-- Returns a function to draw menu items
--
-- col_names should contain an array of names for get_col(), a function(self) returning
-- the text to show in to column, or be nil.
-- if col_names is nil self.text is split at tabulators (\t) and shown in columns
--
-- col_width should contain the width of the columns. If it is empty or missing values
-- the remaining space is equally divided between the remaining columns
function draw_column_item(col_names,col_width)
return function(self,ass,left,top,width,height)
local i
local l=left
local n=l
local cols=col_names
if cols == nil then
cols = toArray(self.text:gmatch("[^\t]+"))
end

for i=1,#cols do
local text
if col_names == nil then
text=cols[i]
elseif type(col_names[i]) == "function" then
text = col_names[i](self)
else
text=get_col(self,col_names[i])
end
if col_width and col_width[i] then
n=l+col_width[i]
else
n=l+(left+width-l)/(#cols-i+1)
end
if text ~= nil then
ass:pos(l,top)
ass_clip(ass,l,top,n-2,top+height)
ass_scale_font(ass,80)
ass:append(tostring(text))
ass:new_event()
end
l=n
end
end
end

function draw_tabbed_column_item(self,ass,left,top,width,height)
mp.log("info","draw_tabbed_column_item")
local cols = toArray(self.text:gmatch("[^\t]+"))
local pos = 0
local l = left
local n
local tabsize = 4
for i=1,#cols do
local text=cols[i]
if text ~= nil then
pos = (math.floor((pos + text:len())/tabsize)+1)*tabsize
n = left + math.floor(pos*config.osd_font_pixel_per_char*0.8)
ass:pos(l,top)
ass_clip(ass,l,top,
(n>left+width and left+width or n)-2,top+height)
ass_scale_font(ass,80)
ass:append(tostring(text))
ass:new_event()
end
l=n
end
end

function action_record(nstate,event)
local timer=event.timer
local cid=event.cid
if timer ~= nil then
-- edit timer
new_state(create_edit_timer_menu_state(timer))
else
-- new timer
nstate.message="Creating timer..."
update_osd()
timer=timer_from_event(event)
local ret = send_update_timer(timer)
if ret then
nstate.confirm="Error: "..tostring(ret)
nstate.confirm_action=function() end
nstate.message=nil
update_osd()
else
nstate.message="Updating..."
update_osd()
mp.add_timeout(0.1,function()
load_timers()
match_timer_to_event(timerinfo[cid],epginfo[cid])
nstate.message=nil
update_osd()
end)
end
end
end

function create_epg_channel_schedule_menu_state(channel_idx)
local c=channels[channel_idx]
local cid=c.id
local items={}
local nstate= new_menu_state("menu_schedule",items)
local draw_item = draw_column_item(
{'einfo.start_date_str','einfo.start_time_str','einfo.timer_status','einfo.title'},
{85,50,15})

local function update_items()
local schedule=epginfo[cid]
while #items>0 do
table.remove(items)
end
if schedule ~= nil then
for i,v in pairs(schedule) do
table.insert(items,{
text=date_format_epg(v),
action = function()
new_state(new_show_epgs_state(v,c))
end,
draw = draw_item,
einfo = v,
cinfo = c,
})
end
end
end
update_items()

mp.add_timeout(0.1,function()
load_epg_channel(cid)
update_items()
nstate.message=nil
update_osd()
end)
nstate.header="Schedule "..c.name
nstate.message="Loading..."
nstate.red_name="Record"
nstate.red_action=function(self)
local event = self.items[self.selected_item].einfo
action_record(nstate, event)
end
nstate.green_name="Now"
nstate.green_action=function(self)
state_back()
new_state(create_epg_now_next_menu_state("epg_now", get_epgnow, channel_idx))
end
nstate.yellow_name="Next"
nstate.yellow_action=function(self)
state_back()
new_state(create_epg_now_next_menu_state("epg_next", get_epgnext, channel_idx))
end
nstate.blue_name="Switch"
nstate.blue_action=function(self)
state_back_to("livetv")
switch_channel(items[self.selected_item].cinfo.idx)
end
return nstate
end

function create_epg_now_next_menu_state(name,get_epg_fct, sel_cidx)
local items={}
local nstate= new_menu_state(name,items)
if sel_cidx == nil then sel_cidx = channel_idx end
local draw_item=draw_column_item(
{'cinfo.name',function(self) return print_time(self.einfo.start) end,'einfo.title'},
{100,55}
)
for i=1,#channels do
local c = channels[i]
if c['id'] and c['name'] then
local e = get_epg_fct(c['id'])
if e then
table.insert(items,{
text=c['name']..format_epg(e),
action = function()
new_state(new_show_epgs_state(e,c))
end,
draw = draw_item,
einfo = e,
cinfo = c,
})
if sel_cidx == c.idx then
nstate.selected_item=#items
end
end
end
end
nstate.blue_name="Switch"
nstate.blue_action=function(self)
state_back_to("livetv")
switch_channel(items[self.selected_item].cinfo.idx)
end
nstate.yellow_name="Schedule"
nstate.yellow_action=function(self)
state_back()
new_state(create_epg_channel_schedule_menu_state(
items[self.selected_item].cinfo.idx))
end
if name=="epg_now" then
nstate.green_name="Next"
nstate.green_action=function(self)
state_back()
new_state(create_epg_now_next_menu_state("epg_next", get_epgnext,
items[self.selected_item].cinfo.idx))
end
else
nstate.green_name="Now"
nstate.green_action=function(self)
state_back()
new_state(create_epg_now_next_menu_state("epg_now", get_epgnow,
items[self.selected_item].cinfo.idx))
end
end
return nstate
end

local function playback_handle_key(self,key)
mp.log("info","state name "..self.name)
local temporarily_show_info = function()
mp.log("info","temp_show_info "..tostring(self.hide_osd_timeout))
if self.hide_osd_timeout~=nil or self.update_osd==nil then
self.hide_osd_timeout = config.show_info_timeout
self.update_osd = show_playback_info
update_hide_osd_timeout()
update_osd()
end
end
if key=="BLUE" then
state_back_to("livetv")
switch_channel(channel_idx)
elseif key=="BS" then
state_back()
switch_channel(channel_idx)
elseif key=="DOWN" or key=="UP" then
mp.command("cycle pause")
temporarily_show_info()
elseif key=="YELLOW" then
mp.command("no-osd seek +30")
temporarily_show_info()
elseif key=="MENU" then
state_main_menu.selected_item=1
new_state(state_main_menu)
elseif key=="GREEN" then
mp.command("no-osd seek -30")
temporarily_show_info()
elseif key=="ENTER" then
if self.update_osd==nil then
self.update_osd=show_playback_info
self.hide_osd_timeout=nil
else
self.update_osd=nil
end
update_osd()
end
end

function create_playback_state(rinfo)
local state_playback = {
name = "playback",
handle_key = playback_handle_key,
update_osd = nil,
rinfo = rinfo,
update_osd_timeout=1,
hide_osd_timeout = config.show_info_timeout,
update_osd = show_playback_info,
}
return state_playback
end

function read_info_file(self)
local file = io.open(self.info_file,"r")
if file then
local lines=file:read("*a")
local event={}
for v in lines:gmatch("[^\r\n]+") do
event = parse_event(event,v)
end
return event.description,format_epg(event),event.subtitle
else
mp.log("info","info file not found")
end
end

function check_for_vdr_recording(dir,name)
local rinfos={}
local dirname=utils.join_path(dir,name)
local dirs = utils.readdir(dirname,"dirs")
if dirs == nil then dirs={} end
for i,v in pairs(dirs) do
if ends_with(v,".rec") then
local rdir=utils.join_path(dirname,v)
local files = utils.readdir(rdir,"files")
local rinfo={url={},name=name}
if files == nil then files={} end
for j,w in pairs(files) do
if ends_with(w,".ts") then
table.insert(rinfo.url,utils.join_path(rdir,w))
elseif w:sub(1,1)=="0" and ends_with(w,".vdr") then
-- old recoding
table.insert(rinfo.url,utils.join_path(rdir,w))
elseif w=="info" or w=="info.vdr" then
rinfo.info_file=utils.join_path(rdir,w)
rinfo.info=read_info_file
end
end
if #rinfo.url>0 then
table.sort(rinfo.url)
table.insert(rinfos,rinfo)
end
end
end
return rinfos
end

local function show_info_action(self)
local item=self.items[self.selected_item]
local state={
name="Media Info",
update_osd=show_text,
text="No info",
message="Loading...",
handle_key=function()
state_back()
end,
}
mp.add_timeout(0.1,function()
if item==nil or item.rinfo==nil then
-- error?
elseif item.rinfo.info then
state.text,state.title,state.subtitle = item.rinfo:info()
elseif item.rinfo.url then
local file =io.open(item.rinfo.url:sub(1,-1-item.rinfo.ext:len())..".txt","r")
if file then
state.text=file:read("*a")
else
mp.log("info","No filename.txt file found")
end
end
state.message=nil
if state.text==nil then state.text="No Info" end
update_osd()
end)
new_state(state)
end

function create_show_media_state(dirname)
local update_items=function()
local items={}
local dirs = utils.readdir(dirname,"dirs")
if dirs == nil then dirs={} end
for i,v in pairs(dirs) do
local vpath=utils.join_path(dirname,v)
local rinfos = check_for_vdr_recording(dirname,v)
if #rinfos>0 then
for j,w in pairs(rinfos) do
table.insert(items, {
text=tostring(w.name),
rinfo=w,
action=function()
playback_recording(w)
new_state(create_playback_state(w))
end
})
end
else
table.insert(items,{
text=tostring(v),
action=function()
new_state( create_show_media_state(vpath))
end,
})
end
end
local files=utils.readdir(dirname,"files")
if files == nil then files={} end
for i,v in pairs(files) do
V=v:upper()
for j,w in pairs(config.media_extensions) do
if ends_with(V,w) then
local rinfo={
name=tostring(v),
url=utils.join_path(dirname,v),
ext=w,
}
table.insert(items,{
text=tostring(v),
rinfo=rinfo,
action=function()
playback_recording(rinfo)
new_state(create_playback_state(rinfo))
end,
})
end
end
end
return items
end
local state=new_menu_state("Media",update_items)
state.blue_name="Info"
state.blue_action=show_info_action
return state
end

function create_recordings_show_items(recordings)
items={}
for i,r in pairs(recordings) do
if r['name'] ~= nil then
local text=r['title']
if r['time'] then text = r['time'].." "..text end
if r['day'] then text = r['day'].." "..text end
table.insert(items,{
text = text,
action = function(self)
if #self.rinfo >0 then
new_state( new_menu_state("Recordings",
create_recordings_show_items(self.rinfo)
))
else
playback_recording(self.rinfo)
new_state(create_playback_state(self.rinfo))
end
end,
rinfo = r,
})
end
end
return items
end

function create_timers_show_items(timers)
items={}
local draw_item=draw_column_item(
{'tinfo.enabled_str','tinfo.chno','tinfo.day_str','tinfo.start_str','tinfo.stop_str','tinfo.name'},
{20,30,70,50,50})
for i,r in pairs(timers) do
table.insert(items,{
tinfo = r,
draw = draw_item,
action = function(self)
new_state(create_edit_timer_menu_state(self.tinfo))
end,
})
end
return items
end

-- ************************* Remote OSD ***********************

local vdr_keys= {
UP="Up", DOWN="Down", LEFT="Left", RIGHT="Right",
BS="Back", RED="Red", GREEN="Green", YELLOW="Yellow", BLUE="Blue",
ENTER="Ok", m="MENU",
}

function update_remote_osd(self)
self.osdinfo=parse_svdrposd_lsto(send_svdrp("PLUG svdrposd LSTO"))
self.items=self.osdinfo.items
self.draw_item=draw_tabbed_column_item
if self.osdinfo.text then
self.text=self.osdinfo.text:gsub("|","\n")
else
self.text=""
end
self.selected_item=self.osdinfo.selected
self.red_name=self.osdinfo.red
self.green_name=self.osdinfo.green
self.yellow_name=self.osdinfo.yellow
self.blue_name=self.osdinfo.blue
self.header="Remote OSD: "..tostring(self.osdinfo.title)
mp.log("info","remote osd red: ".. tostring(self.red_name))

if self.items and #self.items>0 then
self.update_osd=show_menu
else
self.update_osd=show_text
end

self.message=nil
update_osd()
end

function remote_osd_handle_key(self,k)
mp.log("info","remote osd key "..k)
mp.log("info","osdinfo "..tostring(self.osdinfo.open))
if k=="BS" and self.osdinfo.open==false then
state_back()
elseif vdr_keys[k] ~= nil then
mp.log("info","sending key")
self.message="Updating..."
update_osd()
send_svdrp("HITK "..vdr_keys[k])
update_remote_osd(self)
end
end

function create_remote_osd_menu_state()
local state_menu = {
name = "remote_osd",
handle_key = remote_osd_handle_key,
update_osd = show_menu,
items = {},
osdinfo = {
open = false,
},
message = "Loading..",
}
mp.add_timeout(0.1,function()
update_remote_osd(state_menu)
end)
return state_menu
end

-- ************************* Menu Stuff ***********************

function new_menu_state(name,items)
local state_menu = {
name = name,
handle_key = menu_handle_key,
update_osd = show_menu,
}
if type(items) == "function" then
state_menu.message="Loading..."
mp.add_timeout(0.1, function()
state_menu.items=items()
state_menu.message=nil
update_osd()
end)
else
state_menu.items=items
end
return state_menu
end

function create_edit_timer_menu_state(timer)
local update_items=function()
local items={
{ text="Active\t: "..(timer.enabled==0 and "no" or "yes"), },
{ text="Channel\t: "..tostring(timer.chno).." "..tostring(channel_info_from_cid(timer.cid).name),},
{ text="Day\t: "..timer.day},
{ text="Start\t: "..timer.start:sub(1,2)..":"..timer.start:sub(3,4),},
{ text="Stop\t: "..timer.stop:sub(1,2)..":"..timer.stop:sub(3,4),},
{ text="Priority\t: "..timer.priority,},
{ text="Lifetime\t: "..timer.lifetime,},
{ text="File\t: "..timer.name,},
}
return items
end

local timer_menu_state=new_menu_state("Show Timer", update_items)
timer_menu_state.column_width={80}
timer_menu_state.red_name="On/Off"
timer_menu_state.red_action=function(self)
timer_menu_state.message="Updating timer..."
update_osd()
toggle_timer_onoff(timer)
send_update_timer(timer)
load_timers()
match_timer_to_event(timerinfo[cid],epginfo[cid])
self.items=update_items()
timer_menu_state.message=nil
update_osd()
end
return timer_menu_state
end

function confirm_action(msg,action)
return function(self)
self.confirm=msg
self.confirm_action=action
update_osd()
end
end

function create_timer_menu_state()
local update_items= function()
local timers=load_timers();
return create_timers_show_items(timers)
end
local timer_menu_state=new_menu_state("Timers", update_items)

timer_menu_state.red_name="On/Off"
timer_menu_state.red_action=function(self)
timer_menu_state.message="Updating timer..."
update_osd()
local timer=self.items[self.selected_item].tinfo
toggle_timer_onoff(timer)
local ret = send_update_timer(timer)
if ret then
timer_menu_state.confrim="Error: "..tostring(ret)
timer_menu_state.confirm_action=function() end
end
self.items=update_items()
timer_menu_state.message=nil
update_osd()
end
timer_menu_state.yellow_name="Delete"
timer_menu_state.yellow_action=confirm_action("Are you sure? Press OK to delete",function(self)
timer_menu_state.message="Deleting timer..."
update_osd()
local ret=send_delete_timer(self.items[self.selected_item].tinfo)
if ret then
timer_menu_state.confrim="Error: "..tostring(ret)
timer_menu_state.confirm_action=function() end
end
self.items=update_items()
timer_menu_state.message=nil
update_osd()
end)
timer_menu_state.blue_name="Info"
timer_menu_state.blue_action=function(self)
timer_menu_state.message="Loading..."
update_osd()
local timer=self.items[self.selected_item].tinfo
load_epg_channel(timer.cid)
local state=new_show_epgs_state(timer.event,
channel_info_from_cid(timer.cid))
timer_menu_state.message=nil
new_state(state)
end
return timer_menu_state
end

function init_main_menu()
main_menu_items=
{
{
text="Schedule",
action = function()
local nstate=create_epg_channel_schedule_menu_state(channel_idx)
new_state(nstate)
end,
},
{
text="Timers",
action = function()
new_state(create_timer_menu_state())
end,
},
}
if config.vdr_video_dir:len()==0 then
-- use streamdev for recordings
table.insert( main_menu_items, #main_menu_items+1, {
text="Recordings",
action = function()
local update_items=function(self)
--local recordings=parse_lstr(send_svdrp("lstr"))
local recordings=parse_ext3mu(send_webrequest("/recordings.m3u"))
recordings = collect_directories(recordings)
return create_recordings_show_items(recordings)
end
local state=new_menu_state("Recordings", update_items)
-- state.blue_name="Info"
-- state.blue_action=show_info_action
-- state.yellow_name="Remove"
-- state.yellow_action=confirm_action("Are you sure? Press OK to remove",
-- function(self)
-- local item=self.items[self.selected_item]
-- self.message="Deleting recording..."
-- update_osd()
-- local error_msg=check_for_errors(send_svdrp(
-- string.format("DELR %d",item.rinfo.idx)))
-- if error_msg then
-- self.confirm="Error: "..error_msg
-- self.confirm_action=function() end
-- end
-- self.items=update_items()
-- self.message=nil
-- self:update_state()
-- update_osd()
-- end)
state.update_state = update_state_disk_header
new_state(state)
end,
})
else
-- vdr video dir is locally mounted, read it directly
table.insert( main_menu_items, #main_menu_items+1, {
text="Recordings",
action = function()
new_state( create_show_media_state(config.vdr_video_dir) )
end
})
end

if config.media_dir:len()>0 then
table.insert( main_menu_items,#main_menu_items+1,{
text="Media",
action = function()
new_state( create_show_media_state(config.media_dir) )
end,
})
end
state_main_menu = new_menu_state("main_menu", main_menu_items)
state_main_menu.update_state = update_state_disk_header

state_livetv = {
name = "livetv",
handle_key = livetv_handle_key,
update_osd = nil,
}


state_channel_info = {
name = "channel_info",
handle_key = livetv_handle_key,
update_osd = show_channel_info,
timeout = 5,
}
end

function channel_str_to_chidx(channel_str)
local chidx
if channel_str then
chidx=tonumber(channel_str)
mp.log("info","1 startup_channel "..tostring(chidx))
if #channels>0 and chidx then
-- translate channel number to channel idx
chidx=chno_to_idx[chidx]
end
end
if (chidx== nil and #channels>0 and channel_str and channel_str:len()>1) then
-- channel given as channel id
chidx=chid_to_idx[channel_str]
end
return chidx
end

function do_startup(url)
init_main_menu()

mp.add_key_binding("F1",'vdrkeyRED',key("RED"))
mp.add_key_binding("F2",'vdrkeyGREEN',key("GREEN"))
mp.add_key_binding("F3",'vdrkeyYELLOW',key("YELLOW"))
mp.add_key_binding("F4",'vdrkeyBLUE',key("BLUE"))
mp.add_key_binding("0",'vdrkey0',key(0))
mp.add_key_binding("1",'vdrkey1',key(1))
mp.add_key_binding("2",'vdrkey2',key(2))
mp.add_key_binding("3",'vdrkey3',key(3))
mp.add_key_binding("4",'vdrkey4',key(4))
mp.add_key_binding("5",'vdrkey5',key(5))
mp.add_key_binding("6",'vdrkey6',key(6))
mp.add_key_binding("7",'vdrkey7',key(7))
mp.add_key_binding("8",'vdrkey8',key(8))
mp.add_key_binding("9",'vdrkey9',key(9))
mp.add_key_binding("UP",'vdrkeyUP',key("UP"),{repeatable=true})
mp.add_key_binding("DOWN",'vdrkeyDOWN',key("DOWN"),{repeatable=true})
mp.add_key_binding("LEFT",'vdrkeyLEFT',key("LEFT"),{repeatable=true})
mp.add_key_binding("RIGHT",'vdrkeyRIGHT',key("RIGHT"),{repeatable=true})
mp.add_key_binding("ENTER",'vdrkeyENTER',key("ENTER"))
mp.add_key_binding("BS",'vdrkeyBACK',key("BS"))
mp.add_key_binding("m",'vdrkeyMENU',key("MENU"))

local host,port,channel=url:match("vdrstream://([^:/]*)(:?[%d]*)(/?.*)")
if host and host:len()>0 then
config.host=host
end
if port and port:len()>1 then
config.streamdev_port=tonumber(port:sub(2))
end
mp.log("info","VDR host:"..config.host)
mp.log("info","VDR svdrp port:"..config.svdrp_port)
mp.log("info","VDR streamdev port:"..config.streamdev_port)
vdruri="http://"..config.host..":"..config.streamdev_port.."/TS/"

-- set parameters to optimize channel switch time
--mp.set_property("cache-secs",1)
mp.set_property("demuxer-lavf-analyzeduration",1)
mp.set_property("ytdl","no")
mp.set_property("keep-open","yes")
mp.set_property("idle","yes")
mp.set_property("prefetch-playlist","yes")
mp.set_property("force-window","yes")

get_channels()
channel=channel:sub(2)
channel_idx=channel_str_to_chidx(channel)
if channel_idx==nil then
channel_idx=channel_str_to_chidx(config.startup_channel)
end
if channel_idx==nil then
channel_idx=1
end
-- load epg in background
mp.add_timeout(1,function()
load_timers()
load_epg_now()
load_epg_next()
match_nownext_timer_to_event()
mp.log("info","finished epg")
update_osd()
check_for_plugins()
end)
-- periodically update epg
epg_timer = mp.add_periodic_timer(config.epg_nownext_update_time,function()
load_timers()
-- only update "next" event, old "next" events become "now" events
load_epg_next()
match_nownext_timer_to_event()
end)

new_state( state_livetv )
end

mp.add_hook("on_load", 50, on_start)
(8-8/8)