Index: contrib/recordflags.py ================================================================== --- contrib/recordflags.py +++ contrib/recordflags.py @@ -1,34 +1,36 @@ # encoding: utf-8 # api: streamtuner2 # title: Recording options # description: Allows to set streamripper/fIcy options before recording -# version: 0.5 +# version: 0.7 +# depends: streamtuner2 > 2.2.0 # conflicts: continuous_record # priority: optional # config: # { name: recordflags_auto, type: bool, value: 1, description: Apply options automatically once saved. } # { name: recordflags_row, type: select, value: record_flags, select: "record_flags|extras", description: Station field for saved options. } # { name: recordflags_dir, type: str, value: "", description: Default output directory. } # type: handler # category: ui # -# Hijacks the ✪ record button, presents an option dialog to set various +# Hijacks the ● record button, presents an option dialog to set various # streamripper options. Allows to set an output directory or single-file # recording for example. # # Reuses the known option scheme from the config window. Which is perhaps # less pretty than a custom dialog, but allows to show options for different # download/recording tools. # +# Note that predefining -flags in the Apps/Recording config table might +# conflict with per-stream options. In particular avoid a -d directory +# default for streamripper; and use this plugins´ option instead. +# # ToDo: -# → detect different record apps (fPls, wget, youtube-dl, etc.) -# → implement fPls options etc. # → override main.record() instead of action.record # → eventually strip defaults such as `-d ../dir` from conf.record; -# using action append= param now, thus no rewriting of assoc dict; -# so this should rather go into documenation +# using action append= param now, thus no rewriting of assoc dict # import re import os @@ -43,62 +45,64 @@ # hook record button / menu / action class recordflags (FeaturePlugin): # settings cfg_widget_pfx = "recordoptions_config_" - widgets = {} + widgets = {} + + # available options per recording tool flag_meta = { "streamripper": { "title": "streamripper", "priority": "required", "type": "app", "category": "recording", "version": "1.64.6", - "description": "Standard radio stream recording app", + "description": "Standard radio/stream recording tool", "config": [ - { "name": "A", "arg": "-A", "type": "bool", "description": "➖𝘼 Don't write individual tracks/MP3s. And whatever..... fill text", "value": False }, + { "name": "A", "arg": "-A", "type": "bool", "description": "➖𝘼 Don't split individual tracks/MP3s", "value": False }, { "name": "a", "arg": "-a", "type": "str", "description": "➖𝙖 Single MP3 output file", "value": "" }, { "name": "dir", "arg": "-d", "type": "str", "description": "➖𝙙 Destination directory", "value": "" }, { "name": "D", "arg": "-D", "type": "str", "description": "➖𝘿 Filename pattern", "value": "" }, { "name": "s", "arg": "-s", "type": "bool", "description": "➖𝙨 No subdirectories for each stream", "value": False }, { "name": "t", "arg": "-t", "type": "bool", "description": "➖𝙩 Never overwrite incomplete tracks", "value": False }, { "name": "T", "arg": "-T", "type": "bool", "description": "➖𝙏 Truncate duplicated incompletes", "value": False }, - { "name": "o", "arg": "-o", "type": "select", "description": "➖𝙨 Incomplete track overwriting", "select": "|always|never|larger|version", "value": "" }, + { "name": "o", "arg": "-o", "type": "select", "description": "➖𝙤 Incomplete track overwriting", "select": "|always|never|larger|version", "value": "" }, { "name": "l", "arg": "-l", "type": "int", "description": "➖𝙡 Seconds to record", "value": 3600 }, { "name": "M", "arg": "-M", "type": "int", "description": "➖𝙈 Max megabytes to record", "value": 16 }, - { "name": "xs2", "arg": "--xs2", "type": "bool", "description": "➖➖𝙭𝙨𝟮 new pause detection", "value": False }, - { "name": "xsnone", "arg": "--xs-none", "type": "bool", "description": "Dont't search for/split on track silence", "value": False }, + { "name": "xs2", "arg": "--xs2", "type": "bool", "description": "➖➖𝙭𝙨𝟮 New pause detection algorithm", "value": False }, + { "name": "xsnone", "arg": "--xs-none", "type": "bool", "description": "➖➖𝙭𝙨➖𝙣𝙤𝙣𝙚 No silence splitting", "value": False }, { "name": "i", "arg": "-i", "type": "bool", "description": "➖𝙞 Don't add any ID3 tags", "value": False }, - { "name": "id3v1", "arg": "--with-id32v1", "type": "bool", "description": "Add ID3v1 tags", "value": False }, - { "name": "noid3v2", "arg": "--without-id32v2", "type": "bool", "description": "Omit ID3v2 tags", "value": False }, + { "name": "id3v1", "arg": "--with-id3v1", "type": "bool", "description": "➖➖𝙬𝙞𝙩𝙝➖𝙞𝙙𝟯𝙫𝟭 Add ID3v1 tags", "value": False }, + { "name": "noid3v2", "arg": "--without-id3v2", "type": "bool", "description": "➖➖𝙬𝙞𝙩𝙝𝙤𝙪𝙩➖𝙞𝙙𝟯𝙫𝟮 Omit ID3v2 tags", "value": False }, { "name": "cs_fs", "arg": "--codeset-filesys", "type": "str", "description": "Charset filesystem", "value": "" }, { "name": "cs_id3", "arg": "--codeset-id3", "type": "str", "description": "Charset ID3 tags", "value": "" }, - { "name": "u", "arg": "-u", "type": "str", "description": "➖𝙪 Useragent to send", "value": "" }, + { "name": "u", "arg": "-u", "type": "str", "description": "➖𝙪 User-agent (browser id)", "value": "" }, { "name": "p", "arg": "-p", "type": "str", "description": "➖𝙥 Url for HTTP proxy to use", "value": "" }, { "name": "r", "arg": "-r", "type": "str", "description": "➖𝙧 Relay server 'localhost:8000'", "value": "" }, { "name": "m", "arg": "-m", "type": "int", "description": "➖𝙢 Timeout for stalled connection", "value": 15 }, - { "name": "debug", "arg": "--debug", "type": "bool", "description": "➖➖𝙙𝙚𝙗𝙪𝙜", "value": False }, + { "name": "debug", "arg": "--debug", "type": "bool", "description": "➖➖𝙙𝙚𝙗𝙪𝙜 Extra verbosity", "value": False }, ] }, "fPls": { "title": "fPls/fIcy", "priority": "required", "type": "app", "category": "recording", "version": "1.0.19", - "description": "Alternative recording tool", + "description": "Alternative station recording tool", "config": [ - { "name": "verbose", "arg": "➖𝙫", "type": "bool", "description": "➖𝙫 Verbose mode", "value": False }, + { "name": "max", "arg": "-M", "type": "int", "description": "➖𝙈 Maximum cumulative playing time", "value": 0 }, + { "name": "loop", "arg": "-L", "type": "int", "description": "➖𝙇 Maximum playlist loops", "value": 0 }, + { "name": "retry", "arg": "-R", "type": "int", "description": "➖𝙍 Maximum per-stream retries", "value": 0 }, + { "name": "redir", "arg": "-l", "type": "int", "description": "➖𝙡 Redirect follow limit", "value": 0 }, + { "name": "fail", "arg": "-T", "type": "int", "description": "➖𝙏 Wait time after failure", "value": 0 }, + { "name": "daemon", "arg": "-i", "type": "int", "description": "➖𝙞 Max network idle seconds", "value": 0 }, + { "name": "authfn", "arg": "-a", "type": "str", "description": "➖𝙛 HTTP auth file (user:pass)", "value": "" }, + { "name": "verbose", "arg": "-v", "type": "bool", "description": "➖𝙫 Verbose mode", "value": False }, { "name": "daemon", "arg": "-d", "type": "str", "description": "➖𝙙 Daemon mode: log file", "value": "" }, - # -L max Maximum playlist loops - # -M time Maximum cumulative playing time - # -P path Specify fIcy executable name/path - # -R max Maximum per-stream retries - # -T time Wait the specified time after each failure - # -a file Provide HTTP credentials (user:pass file) - # -i time Maximum network idle time - # -l num Redirect follow limit + { "name": "ficy", "arg": "-P", "type": "str", "description": "➖𝙋 Path to fIcy", "value": "" }, ] }, "youtube-dl": { "title": "youtuble-dl", "priority": "required", @@ -115,23 +119,45 @@ { "name": "proxy", "arg": "--proxy", "type": "str", "description": "➖𝙥 Proxy", "value": "" }, { "name": "verbose", "arg": "-v", "type": "bool", "description": "➖𝙫 Verbose mode", "value": False }, { "name": "ipv4", "arg": "-4", "type": "bool", "description": "➖𝟰 Use IPv4", "value": False }, { "name": "ipv6", "arg": "-6", "type": "bool", "description": "➖𝟲 Use IPv6", "value": False }, ] - } + }, + "wget": { + "title": "wget", + "priority": "required", + "type": "app", + "category": "download", + "version": "1.15", + "description": "HTTP download utility", + "config": [ + { "name": "c", "arg": "-c", "type": "bool", "description": "➖𝙘 Continue partial downloads.", "value": True }, + { "name":"nc", "arg":"-nc", "type": "bool", "description": "➖𝙣𝙘 No-clobber, keep existing files.", "value": False }, + { "name": "N", "arg": "-N", "type": "bool", "description": "➖𝙉 Only fetch newer files", "value": False }, + { "name": "O", "arg": "-O", "type": "str", "description": "➖𝙊 Output to file", "value": "" }, + { "name": "v", "arg": "-v", "type": "bool", "description": "➖𝙫 Verbose mode", "value": False }, + { "name": "S", "arg": "-S", "type": "bool", "description": "➖𝙎 Show response headers", "value": False }, + { "name": "U", "arg": "-U", "type": "str", "description": "➖𝙐 Useragent to send", "value": "" }, + ] + }, } # current selection (dialog only runs once anyway, so we can keep flags in same object) app = "streamripper" argmap = {} # "--xs2" => "xs2" namemap = {} # "xs2" => "--xs2" typemap = {} # "xs2" => "bool" defmap = {} # "opt" => "default" + + # parameters from current action.record() call + k = [] # avoids having to pass them around + kw = {} # simplifies gtk callbacks + row = {} # only one active instance anyway # hooks for user interface/handlers - def init2(self, parent, *a, **kw): + def init2(self, parent, *k, **kw): # TEMPORARY WORKAROUND: swap action.record() action.record = self.action_record # BETTER APPROACH: hook record button #parent.on_record_clicked = self.show_window @@ -138,20 +164,20 @@ # add menu entry (for simple triv#1 option) uikit.add_menu([parent.streammenu], "Set single MP3 record -A flag", self.set_cont) # default widget actions parent.win_recordoptions.connect("delete-event", self.hide) + parent.recordoptions_go.connect("clicked", self.do_record) + parent.recordoptions_save.connect("clicked", self.save_only) parent.recordoptions_eventbox.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color("#442211")) # shortcuts self.add_plg = parent.configwin.add_plg # create _cfg widgets self.load_config = parent.configwin.load_config # populate _cfg widgets self.save_config = parent.configwin.save_config # save from _cfg widgets self.recordoptions_cfg = parent.recordoptions_cfg # our vbox widget - self.map_app_args(self.app) - # prepares a few shortcuts def map_app_args(self, app): config = self.flag_meta[app]["config"] self.argmap = { row["arg"]: row["name"] for row in config if row.get("arg") } self.namemap = dict(zip(self.argmap.values(), self.argmap.keys())) @@ -162,60 +188,64 @@ # triv #1 menu option → only saves `-A` flag to row["recordflags"] def set_cont(self, row): row[conf.recordflags_row] = "-A" + # override GtkWindow.destroy/delete-event + def hide(self, *x): + self.parent.win_recordoptions.hide() + return True # hook for action.record def action_record(self, row={}, *k, **kw): kw["assoc"] = conf.record # default if not self.can_handle(row): return action.run_fmt_url(row, *k, **kw) # use saved settings if conf.recordflags_auto and row.get(conf.recordflags_row): - kw["append"] = " " + row[cont.recordflags_row] + kw["append"] = row[conf.recordflags_row].strip() return action.run_fmt_url(row, *k, **kw) # else bring up win_recordoptions - else: - self.show_dialog(row, *k, **kw) # (row, audioformat, source, assoc, append=append) + else: + self.k = k # stash away args: audioformat, source, assoc, append + self.kw = kw + self.row = row + self.show_dialog(self.row) # only handle audio/* streamripper formats def can_handle(self, row): - # check for general MIME types - #if not row.get("format") in ("audio/mpeg", "audio/aac", "audio/ogg"): - # return False # search for configured (flag_meta) apps in conf.record["audio/*"] dict rx_apps = "\\b(?i)(" + ("|".join(self.flag_meta.keys())) + ")\\b" - #print rx_apps cmd = action.mime_app(row.get("format", "audio/*"), conf.record) match = re.findall(rx_apps, cmd or "") log.PROC(cmd) + # if both mime matched, and cmd in supported apps: if cmd and match: log.STAT(match) self.app = match[0] self.map_app_args(self.app) return True return False - # overriden handler - def do_record(self, row, audioformat="audio/mpeg", source="href", assoc={}, append=None): - self.hide() - append = self.args_from_configwin() - #print append - log.EXEC(action.run_fmt_url, row, audioformat, source, assoc or conf.record, append) - #action.run_fmt_url(row, audioformat, source, assoc, append=append) - + # store current dialog settings into row[], invoked by [save] button + def save_only(self, *x): + self.row[conf.recordflags_row] = self.args_from_configwin() + self.parent.channel().save() + + # overriden handler, chains to actual recording, invoked by [record] button + def do_record(self, *x): + self.kw["append"] = self.args_from_configwin().strip() + action.run_fmt_url(self.row, *self.k, **self.kw) + self.hide() # option window - def show_dialog(self, row, *k, **kw): + def show_dialog(self, row): p = self.parent # set labels, connect buttons p.recordoptions_title.set_text(row["title"][0:50]) p.recordoptions_url.set_text(row["url"][0:50]) - p.recordoptions_go.connect("clicked", lambda *x: self.do_record(row, *k, **kw)) - p.recordoptions_save.connect("clicked", lambda *x: self.save_only(row, *k, **kw)) # add option widgets self.load_config_widgets(row, self.app, p) # show window p.win_recordoptions.show() @@ -263,23 +293,12 @@ #-- convert { name=>value, .... } dict into "--arg str" def args_from_configdict(self, loaded_config): s = "" for name, val in loaded_config.items(): default = str(self.defmap.get(name)) - if val in (False, None, "", default): + if val in (False, None, "", 0, "0", default): continue arg = self.namemap[name] s = s + " " + arg if isinstance(val, (str, unicode)): # type == "bool" check here(...) s = s + " " + val return s - - # store current dialog settings into row[] - def save_only(self, row, *k, **kw): - row[conf.recordflags_row] = self.args_from_configwin() - self.parent.channel().save() - - # override GtkWindow.destroy/delete-event - def hide(self, *x): - self.parent.win_recordoptions.hide() - return True -