Internet radio browser GUI for music/video streams from various directory services.

⌈⌋ ⎇ branch:  streamtuner2


Artifact [3bfa1b4ae8]

Artifact 3bfa1b4ae8518af3fee653599a9da5e3c2b43aa0:


# encoding: utf-8
# api: streamtuner2
# title: Recording timer
# description: Schedules play/record events for bookmarked radio stations.
# type: feature
# category: hook
# depends: kronos, action >= 1.1.1
# version: 0.7.7
# config: 
#   { name: timer_duration, type: select, select: "auto|streamripper|fpls", value: none, description: "Support for time ranges" }
#   { name: timer_crontab, type: bool, value: 0, description: "Utilize cron instead of runtime scheduler. (not implemented yet)" }
# priority: optional
# support: basic
#
# Provides an internal timer, to configure recording and playback times/intervals
# for stations. It accepts a natural language time string when registering a stream.
#
# Context menu > Add timer for station
#
# Programmed events are visible in "timer" under the "bookmarks" channel. Times
# are stored in the description field, and can thus be edited. However, after editing
# times manually, streamtuner2 must be restarted for any changes to take effect.
#
# Allowable time specifications are "Mon,Wed,Fri 18:00-20:00 record"
# or even "Any 7:00-12:00 play". The duration is only honored for
# recording via streamripper or fIcy/fPls currently.
#


from config import *
from channels import *
import bundle.kronos as kronos  # unmaintend pkg, but py3-compatibilized version distributed within bundle/
from uikit import uikit
import action
import copy
import re



# timed events (play/record) within bookmarks tab
class timer:

    # plugin info
    module = "timer"
    meta = plugin_meta()
    
    # configuration settings
    timefield = "playing"
    
    # kronos scheduler list
    sched = None
    
    
    
    # prepare gui
    def __init__(self, parent):
        if not parent:
            return
        conf.add_plugin_defaults(self.meta, self.module)
          
        # keep reference to main window
        self.parent = parent
        self.bookmarks = parent.bookmarks
        
        # add menu
        uikit.add_menu([parent.streammenu, parent.streamactions], "Add timer for station", self.edit_timer, insert=4)
        
        # target channel
        if not self.bookmarks.streams.get("timer"):
            self.bookmarks.streams["timer"] = [{"title":"--- timer events ---"}]
        self.bookmarks.add_category("timer")
        self.streams = self.bookmarks.streams["timer"]
        
        # widgets
        uikit.add_signals(parent, {
            ("timer_ok", "clicked"): self.add_timer,
            ("timer_cancel", "clicked"): self.hide,
            ("timer_dialog", "close"): self.hide,
            ("timer_dialog", "delete-event"): self.hide,
        })

        #-- crontab mode?
        # if "timer_crontab" in conf and conf.timer_crontab:
        #     pass
        # elif "timer_crontab_dequeue" in conf:
        #     pass
        
        #-- prepare scheduler
        self.sched = kronos.ThreadedScheduler()
        for row in self.streams:
            try: self.queue(row)
            except Exception as e: log.ERR("queuing error", e)
        self.sched.start()


    # display GUI for setting timespec
    def edit_timer(self, *w):
        self.parent.timer_dialog.show()
        self.parent.timer_value.set_text("Fri,Sat 20:00-21:00 play")

    # done        
    def hide(self, *w):
        return self.parent.timer_dialog.hide()

    # close dialog,get data
    def add_timer(self, *w):
        timespec = self.parent.timer_value.get_text()

        # basic check for consistency
        if not re.match("^(\w{2,3}|[*,;+])+\s+(\d+:\d+)\s*((\.\.+|-+)\s*(\d+:\d+))?\s+(record|play)", timespec):
            self.parent.status("⛔ Danger, Will Robinson! → The given timer date/action is likely invalid. Entry not saved.", timeout=22)
            return

        # hide dialog
        self.parent.timer_dialog.hide()
        row = self.parent.row()
        row = copy.copy(row)

        # add data
        row["listformat"] = "href" #self.parent.channel().listformat
        if row.get(self.timefield):
            row["title"] = row["title"] + " -- " + row[self.timefield]
        row[self.timefield] = timespec

        # store
        self.save_timer(row)
        self.parent.status("Timer saved.")
    
    
    # store row in timer database
    def save_timer(self, row):
        self.streams.append(row)
        self.bookmarks.save()
        self.queue(row)
        pass
        
        
    # Add timer/recording events to scheduler (or later crontab)
    def queue(self, row):
    
        # chk
        if not row.get(self.timefield) or not row.get("url"):
            #log.DATA("NO TIME DATA", row)
            return
    
        # extract timing parameters
        _ = row[self.timefield]
        days = self.days(_)
        time = self.time(_)
        duration = self.duration(_)
        
        # which action
        if row[self.timefield].find("rec")>=0:
            activity, action_method = "record", self.record
        else:
            activity, action_method = "play", self.play
        
        # add
        task = self.sched.add_daytime_task(action_method, activity, days, None, time, kronos.method.threaded, [row], {})

        log.QUEUE( activity, self.sched, (action_method, activity, days, None, time, kronos.method.threaded, [row], {}), task.get_schedule_time(True) )
    
    
    
    # converts Mon,Tue,... into numeric 1-7
    def days(self, s):
        weekdays = ["su", "mo", "tu", "we", "th", "fr", "sa", "su"]
        r = []
        if re.search("any|all|\*", s, re.I):
            return range(0,7)
        for day in re.findall("\w\w+", s.lower()):
            day = day[0:2]
            if day in weekdays:
                r.append(weekdays.index(day))
        return list(set(r))
        
    # get start time 18:00
    def time(self, s):
        r = re.search("(\d+):(\d+)", s)
        return int(r.group(1)), int(r.group(2))
        
    # convert "18:00-19:15" to minutes
    def duration(self, s):
        try:
            r = re.search("(\d+:\d+)\s*(\.\.+|-+)\s*(\d+:\d+)", s)
            start = self.time(r.group(1))
            end = self.time(r.group(3))
            duration = (end[0] - start[0]) * 60 + (end[1] - start[1])
            return int(duration) # in minutes
        except:
            return 0   # no limit
        
    # action wrapper
    def play(self, row, *args, **kwargs):
        action.play(
            row = row,
            audioformat = row.get("format","audio/mpeg"), 
            source = row.get("listformat","href")
        )

    # kronos/sched callback: action wrapper
    def record(self, row, *args, **kwargs):
        log.TIMER("TIMED RECORD", *args)
        
        # extra params
        # make streamripper record a timed broadcast
        duration = self.duration(row.get(self.timefield))
        append = None
        if duration:
            _rec = conf.record.get("audio/*", "")
            if re.search("streamripper", _rec):
                append = "-a %S.%d.%q -l " + str(duration*60) # seconds
            if re.search("fPls|fIcy", _rec, re.I):
                append = "-M " + str(duration) # minutes

        # start recording
        action.record(
            row = row,
            audioformat = row.get("format","audio/mpeg"), 
            source = row.get("listformat","href"),
            append = append,
        )
    
    # kronos/sched callback for testing
    def test(self, row, *args, **kwargs):
        log.TEST("KRONOS", row)


    # crontab handlers
    def cron_queue(self, row):
        pass

    # crontab cleanup
    """
        All streamtuner2 events will be preceded with a `# streamtuner2: …` comment
    """
    def cron_dequeue_all(self):
        pass