diff --git a/README.md b/README.md
index 887121b..13f43fa 100644
--- a/README.md
+++ b/README.md
@@ -144,12 +144,6 @@ Contents - links to doc section for each script here:
- [\[desktop/media\]](#hdr-__desktop_media__)
- - [parec_from_flash](#hdr-parec_from_flash)
- - [pa_track_history](#hdr-pa_track_history)
- - [pa_modtoggle](#hdr-pa_modtoggle)
- - [mpv_icy_track_history](#hdr-mpv_icy_track_history)
- - [icy_record](#hdr-icy_record)
- - [radio](#hdr-radio)
- [toogg](#hdr-toogg)
- [totty](#hdr-totty)
- [split](#hdr-split)
@@ -3105,86 +3099,8 @@ like passing magnet: links to transmission, or processing .torrent files.
#### [\[desktop/media\]](desktop/media)
-Scripts - mostly wrappers around ffmpeg and pulseaudio - to work with (or
-process) various media files and streams.
-
-
-
-##### [parec_from_flash](desktop/media/parec_from_flash)
-
-Creates null-sink in pulseaudio and redirects browser flash plugin audio output
-stream to it, also starting "parec" and oggenc to record/encode whatever happens
-there.
-
-Can be useful to convert video to podcast if downloading flv is tricky for
-whatever reason.
-
-
-
-##### [pa_track_history](desktop/media/pa_track_history)
-
-Queries pa sinks for specific pid (which it can start) and writes "media.name"
-(usually track name) history, which can be used to record played track names
-from e.g. online radio stream in player-independent fashion.
-
-
-
-##### [pa_modtoggle](desktop/media/pa_modtoggle)
-
-Script to toggle - load or unload - pulseaudio module.
-
-For example, to enable/disable forwarding sound over network (e.g. to be played
-in vlc as rtp://224.0.0.56:9875):
-
- % pa_modtoggle module-rtp-send \
- source=alsa-speakers.monitor destination=224.0.0.56 port=9875
- Loaded: [31] module-rtp-send source=alsa-speakers.monitor destination=224.0.0.56 port=9875
-
-Same exact command will unload the module (matching it by module name only), if necessary.
-
-Optional `-s/--status` flag can be used to print whether module is currently loaded.
-
-Uses/requires [pulsectl module], python.
-
-[pulsectl module]: https://github.com/mk-fg/python-pulse-control/
-
-
-
-##### [mpv_icy_track_history](desktop/media/mpv_icy_track_history)
-
-Same as pa_track_history above, but gets tracks when [mpv] dumps icy-\* tags
-(passed in shoutcast streams) to stdout, which should be at the start of every
-next track.
-
-More efficient and reliable than pa_track_history, but obviously mpv-specific.
-
-[mpv]: https://mpv.io/
-
-
-
-##### [icy_record](desktop/media/icy_record)
-
-Simple script to dump "online radio" kind of streams to a bunch of separate
-files, split when stream title (as passed in icy StreamTitle metadata) changes.
-
-By default, filenames will include timestamp of recording start, sequence
-number, timestamp of a track start and a stream title (in a filename-friendly form).
-
-Sample usage: `icy_record --debug -x https://pub5.di.fm/di_vocaltrance`
-
-Note that by default dumped streams will be in some raw adts format (as streamed
-over the net), so maybe should be converted (with e.g. ffmpeg) afterwards.
-
-This doesn't seem to be an issue for at least mp3 streams though, which work
-fine as "MPEG ADTS, layer III, v1" even in dumb hardware players.
-
-
-
-##### [radio](desktop/media/radio)
-
-Wrapper around mpv_icy_track_history to pick and play hard-coded radio streams
-with appropriate settings, generally simplified ui, logging and echoing what's
-being played, with a mute button (on SIGQUIT button from terminal).
+Scripts - mostly wrappers around ffmpeg and pulseaudio - to work with
+(or process) various media files and streams.
diff --git a/desktop/media/icy_record b/desktop/media/icy_record
deleted file mode 100755
index c11971a..0000000
--- a/desktop/media/icy_record
+++ /dev/null
@@ -1,183 +0,0 @@
-#!/usr/bin/env python2
-# -*- coding: utf-8 -*-
-from __future__ import print_function
-
-import itertools as it, operator as op, functools as ft
-from contextlib import closing
-from collections import deque
-from datetime import datetime
-from threading import Event
-import os, sys, re, signal
-
-import requests
-
-p = lambda fmt,*a,**k: print(fmt.format(*a,**k), file=sys.stderr)
-try:
- import pyaml
- pp = lambda data: pyaml.dump(data, sys.stderr)
-except ImportError:
- from pprint import pprint as pp
-
-
-def find_frame_n(chunk0=None, chunk1=None, n_target=None):
- '''Locates start of adts frame as close to the middle of chunk0 as possible.
- Presumably stream title changes between chunk0
- and chunk1, so new track is expected to start somewhere in chunk0.
- Returns 0 <= n < (len(chunk0) + len(chunk1)).'''
- # http://wiki.multimedia.cx/index.php?title=ADTS
- chunk0, chunk1 = chunk0 or '', chunk1 or ''
- if n_target is None: n_target = len(chunk0) // 2
- nn, n, n_offset = None, -1, None
- chunk = chunk0 + chunk1
- for m in xrange(len(chunk)):
- n = chunk.find('\xff', n+1)
- if n == -1: break
- if ord(chunk[n+1]) >> 4 != 0xf: continue
- n_offset_new = abs(n_target - n)
- if n_offset is None or n_offset_new < n_offset: nn, n_offset = n, n_offset_new
- else: break
- else: raise RuntimeError(chunk)
- if nn is None:
- log.debug('Failed to find ADTS frame header in supplied chunks')
- nn = n_target
- return nn
-
-
-filename_subs = {
- r'[\\/]': '_', r'^\.+': '_', r'[\x00-\x1f]': '_', r':': '-_',
- r'<': '(', r'>': ')', r'\*': '+', r'[|!"]': '-', r'[\?\*]': '_',
- '[\'’]': '', r'\.+$': '_', r'\s+$': '', r'\s': '_' }
-filename_subs = list(
- (re.compile(k), v) for k,v in filename_subs.viewitems() )
-
-def filename_process(name):
- for sub_re, sub in filename_subs: name = sub_re.sub(sub, name)
- return name
-
-def ts_format(fmt=None, ts=None):
- if not fmt: return ''
- if not ts: ts = datetime.now()
- return ts.strftime(fmt)
-
-
-def main(args=None):
- import argparse
- parser = argparse.ArgumentParser(
- description='Follow specified icy (shoutcast/icecast/*cast)'
- ' stream and dump individual tracks from it to a separate files.')
- parser.add_argument('url', help='URL of an http stream to process.')
- parser.add_argument('-d', '--dir', metavar='path',
- help='Directory to store tracks in (default: current dir).')
- parser.add_argument('-f', '--filename-format',
- metavar='format', default='{ts_start}__{n:03d}__{ts}__{name}.mp3',
- help='Filename template to use for each track (default: %(default)r).')
- parser.add_argument('-t', '--ts-format',
- metavar='strftime_format', default='%Y%m%d-%H%M%S',
- help='Format to use for timestamps in --filename-format'
- ' (as parsed by datetime.strftime in python, default: %(default)s).')
- parser.add_argument('-x', '--cut-on-meta-blocks', action='store_true',
- help='Assume that media blocks always belong to track in the following icy-meta.'
- ' Seem to be the case with at least some online radios.')
- parser.add_argument('-1', '--skip-first', action='store_true',
- help='Skip dumping first track in the stream, which should be incomplete.')
- parser.add_argument('-s', '--skip-regexp', metavar='title_regexp',
- help='Skip dumping tracks where title matches specified (python) regexp.')
- # parser.add_argument('-c', '--convert', action='store_true',
- # help='Convert each track to ogg in the background after it is stored.')
- parser.add_argument('--debug', action='store_true', help='Verbose operation mode.')
- opts = parser.parse_args(sys.argv[1:] if args is None else args)
-
- global log
- import logging
- logging.basicConfig(
- level=logging.DEBUG if opts.debug else logging.WARNING,
- format='%(asctime)s :: %(levelname)s :: %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S' )
- log = logging.getLogger()
-
- if opts.dir: os.chdir(os.path.expanduser(opts.dir))
- skip_regexp = opts.skip_regexp and re.compile(opts.skip_regexp)
-
- r = requests.get(opts.url, headers={'Icy-MetaData': '1'}, stream=True)
- with closing(r):
- r.raise_for_status()
- bs = int(r.headers.get('icy-metaint') or 0)
- if not bs:
- p('HTTP response is missing "icy-metaint" header, aborting.')
- pp(dict(response=dict(code=r.status_code, headers=dict(r.headers.items()))))
- return 1
- log.debug('icy-metaint block size: %dB', bs)
-
- ts_start = ts_format(opts.ts_format)
- title_err_streak, title_err_streak_max = 0, 10
- title = title_new = None
- chunk = chunk_new = ''
- dst, dst_buff, dst_n, dst_n_iter = None, deque(), 0, iter(xrange(1, 2**30))
- n_target = 0 if opts.cut_on_meta_blocks else None
-
- title_changed = Event()
- signal.signal(signal.SIGQUIT, lambda sig,frm: title_changed.set())
-
- try:
- while True:
- chunk_new = r.raw.read(bs)
-
- if not title_changed.is_set(): dst_buff.append(chunk_new)
- else:
- if not title_new:
- title_new = '{} [#{:03d}]'.format(re.sub(r'^(.*) \[#\d+\]$', r'\1', title), dst_n)
- log.info('Detected stream title change: %r -> %r', title, title_new)
- n = find_frame_n(chunk, chunk_new, n_target=n_target)
- if n >= len(chunk):
- dst_buff.append(chunk)
- n = n - len(chunk)
- chunk, chunk_new = chunk_new[:n], chunk_new[n:]
- dst_buff.append(chunk)
- else:
- chunk, chunk_new = chunk[:n], chunk[n:] + chunk_new
- dst_buff.append(chunk)
- if not opts.skip_first or title is not None: chunk = None
- title = title_new
- title_changed.clear()
-
- if dst_buff and dst:
- while dst_buff: dst.write(dst_buff.popleft())
- if chunk is None and title is not None:
- if dst:
- dst.close()
- dst = None
- dst_buff.clear()
- if skip_regexp and skip_regexp.search(title):
- log.debug('Skipping dump of track title: %r', title)
- else:
- dst_n = next(dst_n_iter)
- name = opts.filename_format.format( ts_start=ts_start, n=dst_n,
- ts=ts_format(opts.ts_format), name=filename_process(title) )
- log.debug('Creating new file: %r', name)
- dst = open(name, 'wb')
- chunk = chunk_new
- if not chunk:
- log.debug('Reached the end of stream, exiting')
- return
-
- title_new_bs = r.raw.read(1)
- if not title_new_bs: continue
- title_new_bs = ord(title_new_bs) * 16
- title_new = r.raw.read(title_new_bs).rstrip('\0')
- if title_new:
- m = re.search(r'\bStreamTitle=\'(.*?)\';', title_new)
- if not m:
- log.error( 'Failed to process stream'
- ' title block (len: %dB): %r', title_new_bs, title_new )
- title_err_streak += 1
- if title_err_streak > title_err_streak_max:
- p('Too many title-parsing errors in a row - probably a desync issue, aborting')
- return 1
- continue
- else: title_new, title_err_streak = m.group(1), 0
- if title_new != title: title_changed.set()
-
- finally:
- if dst: dst.close()
-
-if __name__ == '__main__': sys.exit(main())
diff --git a/desktop/media/mpv_icy_track_history b/desktop/media/mpv_icy_track_history
deleted file mode 100755
index 8209016..0000000
--- a/desktop/media/mpv_icy_track_history
+++ /dev/null
@@ -1,265 +0,0 @@
-#!/usr/bin/env python2
-# -*- coding: utf-8 -*-
-from __future__ import print_function
-
-from subprocess import Popen, PIPE, STDOUT
-from datetime import datetime
-import os, sys, logging, string, re, time, signal, tempfile, atexit
-
-
-# Since mpv-0.6.2, it doesn't seem to accept chars to stdin as control anymore
-# This was used to send "mute" key to it ("m")
-# Following code is the workaround to toggle mute for the stream of mpv pid instead
-
-_bus_cache = None
-def get_bus(srv_addr=None):
- global _bus_cache
- if not srv_addr and _bus_cache is not None: return _bus_cache
- import dbus
- if srv_addr is None:
- srv_addr = os.environ.get('PULSE_DBUS_SERVER')
- if not srv_addr\
- and os.access('/run/pulse/dbus-socket', os.R_OK | os.W_OK):
- # Well-known system-wide daemon socket
- srv_addr = 'unix:path=/run/pulse/dbus-socket'
- if not srv_addr:
- srv_addr = dbus.SessionBus().get_object(
- 'org.PulseAudio1', '/org/pulseaudio/server_lookup1')\
- .Get('org.PulseAudio.ServerLookup1',
- 'Address', dbus_interface='org.freedesktop.DBus.Properties')
- _bus_cache = dbus.connection.Connection(srv_addr)
- return _bus_cache
-
-def dbus_bytes(dbus_arr, strip='\0' + string.whitespace):
- return bytes(bytearray(dbus_arr).strip(strip))
-
-def pa_mute(pid, val, srv_addr=None):
- import dbus
- bus = get_bus(srv_addr=srv_addr)
- streams = bus.get_object(object_path='/org/pulseaudio/core1')\
- .Get( 'org.PulseAudio.Core1', 'PlaybackStreams',
- dbus_interface='org.freedesktop.DBus.Properties' )
- if not streams: return None
- for path in streams:
- stream = dbus.Interface(
- bus.get_object(object_path=path),
- dbus_interface='org.freedesktop.DBus.Properties' )
- props = stream.Get('org.PulseAudio.Core1.Stream', 'PropertyList')
- stream_pid = int(dbus_bytes(props['application.process.id']))
- if stream_pid == pid:
- stream.Set('org.PulseAudio.Core1.Stream', 'Mute', bool(val))
- break
- else:
- logging.getLogger('pactl').warn('Failed to find pa stream for pid: {}'.format(pid))
-
-
-track_max_len = 16384
-mute_prefix_default = 'muted: '
-
-class MpvCtl(object):
-
- player_cmd_base = ['mpv', '--term-status-msg=']
- player = track_last = None
- muted = muted_auto = muted_change = False
- mute_types = 'stdin', 'pulse', 'input-file'
-
- def __init__( self, dst_file=None,
- mute_re=None, mute_delay=None, mute_type='input-file', mute_prefix=None,
- recode=None, line_format=None, ts_format=None ):
- self.log = logging.getLogger('mpvctl')
-
- self.line_format = line_format or '{ts}{mute}{}\n'
- self.ts_format, self.recode = ts_format, recode
- self.mute_re, self.mute_delay = mute_re, mute_delay
- self.mute_type, self.mute_prefix = mute_type, mute_prefix
- assert self.mute_type in self.mute_types, [self.mute_type, self.mute_types]
- if dst_file:
- self.dst = dst = open(dst_file, 'a+b')
- dst.seek(max(0, os.fstat(dst.fileno()).st_size - (track_max_len + 2)))
- last_line = dst.read()
- if '\n' in last_line:
- self.track_last = last_line.rstrip('\r\n').rsplit('\n', 1)[-1].strip()
- else:
- self.dst = sys.stdout
-
- self.install_mute_sig_handler()
- if mute_re: self.install_mute_sig_handler(signal.SIGALRM)
-
- def install_mute_sig_handler(self, sig=signal.SIGQUIT):
- signal.signal(sig, self.send_mute)
-
- def terminate(self):
- if not (self.player and self.player.poll() is None): return
- self.log.debug('Terminating running player instance')
- self.player.terminate()
-
- def send_mute(self, sig=None, frm=None):
- if not self.player: return
- if self.muted_change: return
- self.muted_change = True # racy lock
- self.muted = not self.muted
- auto = sig is True or sig == signal.SIGALRM
- if auto:
- if not self.muted_auto: return
- elif self.muted_auto: self.muted_auto = False # manual action
- if not self.muted: self.muted_auto = False
- self.log.debug( 'Toggling "mute" on a running player'
- ' instance (sig: %s, auto: %s) to: %s', sig, auto, self.muted )
- if self.mute_type == 'input-file':
- with open(self.player.input_fifo, 'wb') as dst:
- dst.write('set mute {}\n'.format(['no', 'yes'][bool(self.muted)]))
- elif self.mute_type == 'stdin': self.player.stdin.write('m')
- elif self.mute_type == 'pulse': pa_mute(self.player.pid, self.muted)
- else: raise ValueError(self.mute_type)
- self.muted_change = False
- return self.muted
-
- def _send_mute_auto(self):
- if not self.mute_delay or self.mute_delay < 0: self.send_mute(True)
- else: signal.alarm(self.mute_delay)
-
- def send_mute_auto(self, enable):
- if enable:
- if self.muted: return
- self.muted_auto = True
- self._send_mute_auto()
- else:
- if not self.muted_auto: return
- self._send_mute_auto()
-
- def track_match(self, line):
- line = line.strip()
- if not line.startswith('icy-title:'): return
- track = line[10:].strip()
- if self.track_last and self.track_last.endswith(track): return
- self.track_last = track
- return track
-
- def track_changed(self, track):
- mute_prefix = ''
- if self.mute_re:
- if self.mute_re.search(track):
- self.send_mute_auto(True)
- if self.mute_prefix: mute_prefix = self.mute_prefix
- elif self.muted_auto: self.send_mute_auto(False)
- if self.recode:
- track = track\
- .decode(self.recode, 'replace')\
- .encode('utf-8', 'backslashreplace')
- # "datetime" allows for more strftime options here than just "time"
- ts = datetime.now().strftime(self.ts_format) if self.ts_format else ''
- self.dst.write(self.line_format.format(track, ts=ts, mute=mute_prefix))
- self.dst.flush()
-
- def track_dump_loop(self, opts):
- player_cmd, player_input = self.player_cmd_base, None
- if self.mute_type == 'input-file':
- player_input = os.path.join(
- tempfile.gettempdir(),
- '.mpv_input.{}.fifo'.format(os.getpid()) )
- os.mkfifo(player_input, 0700) # probably insecure
- atexit.register(os.unlink, player_input)
- player_cmd.extend(['--input-file', player_input])
- elif self.mute_type == 'stdin': player_cmd.append('--input-terminal')
- player_cmd.extend(opts.mpv_args)
-
- ts = time.time()
- while True:
- if self.player:
- player_poll = self.player.poll()
- if player_poll is not None:
- player_poll, self.player = self.player.wait(), None
- if player_poll != 0:
- self.log.error('mpv failed (exit code: %s), exiting', player_poll)
- return player_poll
-
- if not self.player:
- self.player = Popen( player_cmd,
- stdin=PIPE, stdout=PIPE, stderr=STDOUT, preexec_fn=os.setsid )
- self.player.input_fifo = player_input
-
- line_last = None
- for line in iter(self.player.stdout.readline, ''):
- ls = line.rstrip()
- if opts.passthrough and ls != line_last: self.log.debug(ls)
- line_last = ls
- track = self.track_match(line)
- if not track: continue
- self.track_changed(track)
-
-
-def main(args=None):
- import argparse
- parser = argparse.ArgumentParser(
- description='Record whatever is playing in mpv (as supplied by icy-* tags) to some file.'
- ' Sending SIGQUIT (Ctrl+\ from terminal) toggles mute for the playback'
- ' (actually writes "m" char to mpv stdin, which is bound to "mute" by default).')
- parser.add_argument('mpv_args', nargs='+',
- help='Options/arguments (including playback source) to pass to mpv.'
- ' Some terminal-io options will be prepended to these as well.'
- ' Use "--" to make sure its options wont get processed by this wrapper script.')
-
- parser.add_argument('-m', '--mute-regexp', metavar='regexp',
- help='Toggle mute than track name matches'
- ' that regexp, unmuting than name changes again.')
- parser.add_argument('--mute-delay', type=float, metavar='seconds', default=1,
- help='When auto-toggling mute, delay by this many seconds'
- ' (useful for long track crossfades, default: %(default)s).')
- parser.add_argument('--mute-type',
- metavar='type', default='input-file', choices=MpvCtl.mute_types,
- help='Way to mute the stream. Possible choices: stdin, pulse, input-file.'
- ' "stdin" is the "old" way to do it, by writing "m"'
- ' to mpv stdin, doesnt seem to work with newer mpv versions.'
- ' "pulse" will use dbus to mute stream corresponding to'
- ' mpv pid in pulseaudio - reliable, but only works with PA, obviously.'
- ' "input-file" (default) will use fifo and --input-file option for mpv'
- ' to pass "set mute" commands, probably the best way to do it.')
- parser.add_argument('--mute-title-prefix',
- nargs='?', default=False, metavar='prefix',
- help=(
- 'Add prefix (can be specified as optional arg,'
- ' default: {}) to track titles that match --mute-regexp.'
- ' If --line-format is used, passed to it as "mute" keyword.')\
- .format(mute_prefix_default))
-
- parser.add_argument('-d', '--dst-file',
- help='Path to a file to record all the stuff to. If omitted, stdout will be used.')
- parser.add_argument('-f', '--line-format',
- help='Format for each output line. Line itself is passed as the first arg.')
- parser.add_argument('-t', '--timestamp', action='store_true',
- help='Prepend timestamps to each track entry in the output.'
- ' If --line-format is used, passed to it as "ts" keyword.')
- parser.add_argument('--timestamp-format',
- metavar='py_ts_format', default='[%Y-%m-%d %H:%M] ',
- help='Format for timestamp-prefix to be prepended to each line (default: %(default)s).'
- ' Should be compatible with pythons strftime() functions.')
-
- parser.add_argument('-e', '--recode-from', metavar='encoding',
- help='Decode titles from specified encoding and encode to utf-8.')
-
- parser.add_argument('--passthrough',
- action='store_true', help='Pass all mpv stdout lines to debug logging.')
- parser.add_argument('--debug', action='store_true', help='Verbose operation mode.')
- opts = parser.parse_args(sys.argv[1:] if args is None else args)
-
- logging.basicConfig(level=logging.DEBUG if opts.debug else logging.WARNING)
-
- mute_re = None
- if opts.mute_regexp:
- mute_re = opts.mute_regexp
- if mute_re.endswith('$\n'): mute_re = mute_re.rstrip() # common yaml quirk
- mute_re = re.compile(mute_re)
- mute_title_prefix = opts.mute_title_prefix or None
- if opts.mute_title_prefix is None:
- opts.mute_title_prefix = mute_prefix_default
-
- mpvctl = MpvCtl(
- dst_file=opts.dst_file, recode=opts.recode_from,
- line_format=opts.line_format,
- mute_delay=opts.mute_delay, mute_re=mute_re,
- mute_type=opts.mute_type, mute_prefix=opts.mute_title_prefix,
- ts_format=opts.timestamp and opts.timestamp_format )
- try: return mpvctl.track_dump_loop(opts)
- finally: mpvctl.terminate()
-
-if __name__ == '__main__': sys.exit(main())
diff --git a/desktop/media/pa_modtoggle b/desktop/media/pa_modtoggle
deleted file mode 100755
index 6fe39ce..0000000
--- a/desktop/media/pa_modtoggle
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env python3
-
-from pulsectl import Pulse
-import os, sys
-
-
-def main(args=None):
- import argparse
- parser = argparse.ArgumentParser(
- description='Tool to toggle module loading/unloading with same one command.')
- parser.add_argument('module', help='Module name to toggle.')
- parser.add_argument('params', nargs='*', help='Module parameters, if any.')
- parser.add_argument('-s', '--status', action='store_true',
- help='Do not load/unload anything, only show whether module is loaded and exit.')
- opts = parser.parse_args(sys.argv[1:] if args is None else args)
-
- with Pulse('pa-modtoggle') as pulse:
- loaded = mod = None
- for mod in pulse.module_list():
- if mod.name == opts.module:
- loaded = True
- break
- else: loaded = False
-
- if opts.status:
- print('Module {!r} status: {}'.format(
- opts.module, ['disabled', 'enabled'][bool(loaded)] ))
- else:
- if loaded:
- print('Unloaded: [{}] {} {}'.format(mod.index, mod.name, mod.argument))
- pulse.module_unload(mod.index)
- else:
- index = pulse.module_load(opts.module, opts.params)
- print('Loaded: [{}] {} {}'.format(index, opts.module, ' '.join(opts.params)))
-
-if __name__ == '__main__': sys.exit(main())
diff --git a/desktop/media/pa_track_history b/desktop/media/pa_track_history
deleted file mode 100755
index 066be71..0000000
--- a/desktop/media/pa_track_history
+++ /dev/null
@@ -1,191 +0,0 @@
-#!/usr/bin/env python2
-# -*- coding: utf-8 -*-
-from __future__ import print_function
-
-import os, sys, string, re, time
-
-
-track_max_len = 16384
-
-def get_bus(srv_addr=None):
- import dbus
- if srv_addr is None:
- srv_addr = os.environ.get('PULSE_DBUS_SERVER')
- if not srv_addr\
- and os.access('/run/pulse/dbus-socket', os.R_OK | os.W_OK):
- # Well-known system-wide daemon socket
- srv_addr = 'unix:path=/run/pulse/dbus-socket'
- if not srv_addr:
- srv_addr = dbus.SessionBus().get_object(
- 'org.PulseAudio1', '/org/pulseaudio/server_lookup1')\
- .Get('org.PulseAudio.ServerLookup1',
- 'Address', dbus_interface='org.freedesktop.DBus.Properties')
- return dbus.connection.Connection(srv_addr)
-
-def dbus_bytes(dbus_arr, strip='\0' + string.whitespace):
- return bytes(bytearray(dbus_arr).strip(strip))
-
-def _track_for_pid(pid, srv_addr=None):
- bus = get_bus(srv_addr=srv_addr)
- streams = bus.get_object(object_path='/org/pulseaudio/core1')\
- .Get( 'org.PulseAudio.Core1', 'PlaybackStreams',
- dbus_interface='org.freedesktop.DBus.Properties' )
- streams = list( bus.get_object(object_path=path)\
- .Get('org.PulseAudio.Core1.Stream', 'PropertyList')
- for path in streams )
- if not streams: return None
- for stream in streams:
- stream_pid = int(dbus_bytes(stream['application.process.id']))
- if stream_pid == pid: return dbus_bytes(stream['media.name'])
- else: log.warn('Failed to find pa stream for pid: {}'.format(pid))
-
-child_ctl = None
-def track_for_pid(pid, srv_addr=None, dbus_reuse=False):
- if dbus_reuse:
- return _track_for_pid(pid, srv_addr=srv_addr)
-
- # Forking here is to prevent dbus module from keeping state,
- # which seem to prevent it from working on any pulseaudio hiccup
- global child_ctl
- if child_ctl: os.close(child_ctl)
- (r1,w1), (r2,child_ctl) = os.pipe(), os.pipe()
- child_pid, track = os.fork(), None
- if child_pid: # parent
- os.close(w1), os.close(r2)
- try:
- track = os.read(r1, track_max_len + 1)
- os.close(r1)
- os.write(child_ctl, 'x')
- except (OSError, IOError): pass
- try: os.waitpid(child_pid, 0)
- except OSError: pass
- else: # child
- os.close(r1), os.close(child_ctl)
- try:
- track = _track_for_pid(pid, srv_addr=srv_addr)
- os.write(w1, track or '\0')
- assert os.read(r2, 1) == 'x'
- except Exception as err:
- log.warn('Failed to query pa via dbus: %s', err)
- os._exit(0)
- return track if track and track != '\0' else None
-
-
-def proc_list():
- import glob
- cmds = list()
- for p in glob.glob('/proc/*/cmdline'):
- try: pid = int(p.split('/', 3)[2])
- except ValueError: continue # e.g. "self"
- try:
- with open(p) as src: cmd = src.read()
- except (OSError, IOError): continue
- cmd = cmd.strip('\0')
- if cmd: cmds.append((pid, cmd.split('\0')))
- return cmds
-
-def proc_match(regexp, procs=None):
- if procs is None: procs = proc_list()
- for pid, cmd in procs:
- cmd = ' '.join(cmd)
- if re.search(regexp, cmd): return pid
-
-
-def track_dump_loop(opts):
- track_last = None
- if opts.dst_file:
- dst = open(opts.dst_file, 'a+b')
- dst.seek(max(0, os.fstat(dst.fileno()).st_size - (track_max_len + 2)))
- last_line = dst.read()
- if '\n' in last_line:
- track_last = last_line.rstrip('\r\n').rsplit('\n', 1)[-1].strip()
- else: dst = sys.stdout
-
- pid, ts = None, time.time()
- while True:
- if not opts.prog:
- if not pid: pid = proc_match(opts.pgrep) if not opts.pgrep.isdigit() else int(opts.pgrep)
- else:
- player_poll = player.poll() if pid else None
- if not pid or player_poll == 0:
- global err_hook, devnull
- from subprocess import Popen
- player = dict()
- if opts.quiet:
- if not devnull: devnull = open(os.devnull, 'wb')
- player.update(stdout=devnull, stderr=devnull)
- player = Popen(opts.prog, **player)
- pid, err_hook = player.pid, lambda p=player: p.poll() is None and p.terminate()
- elif pid and player_poll is not None:
- log.error('Player app failed (exit code: %s), exiting', player_poll)
- return player_poll
-
- if pid is not None:
- track = track_for_pid(pid, dbus_reuse=opts.once)
- if track:
- if opts.strip: track = re.sub(opts.strip, '', track)
- if track and not (track_last and track_last.endswith(track)):
- track_last = track
- if opts.timestamp: prefix = time.strftime(opts.timestamp_format)
- else: prefix = ''
- dst.write('{}{}\n'.format(prefix, track))
- dst.flush()
-
- else:
- log.error('Failed to get stream pid')
-
- if opts.once: break
- ts, ts_to = time.time(), ts + opts.poll_interval
- while ts_to <= ts: ts_to += opts.poll_interval
- time.sleep(ts_to - ts)
- ts = ts_to
-
-def main(args=None):
- import argparse
- parser = argparse.ArgumentParser(
- description='Record whatever is playing in some pid via pulse to some file.'
- ' Takes "media.name" parameter from PulseAudio Stream and records any changes to it.')
- parser.add_argument('prog', nargs='*', help='Playback app to run.'
- ' Use "--" to make sure its options wont get processed by this wrapper script.')
-
- parser.add_argument('-d', '--dst-file',
- help='Path to a file to record all the stuff to. If omitted, stdout will be used.')
- parser.add_argument('-t', '--timestamp', action='store_true',
- help='Prepend timestamps to each track entry in the output.')
- parser.add_argument('--timestamp-format',
- metavar='py_ts_format', default='[%Y-%m-%d %H:%M] ',
- help='Format for timestamp-prefix to be prepended to each line (default: %(default)s).'
- ' Should be compatible with pythons strftime() functions.')
-
- parser.add_argument('-1', '--once', action='store_true', help='Sample once and exit.')
- parser.add_argument('-i', '--poll-interval',
- type=float, metavar='seconds', default=80,
- help='Interval between sampling track name (default: %(default)s).')
- parser.add_argument('-p', '--pgrep',
- metavar='{pid | cmdline_regexp}', default=r'^mpv\s+',
- help='Grep for this regexp in processes'
- ' to find pid that is used as a player (default: %(default)s).'
- ' If integer is specified, it will be used as a pid without any extra matching.'
- ' Disregarded, if playback app is specified in args.'
- ' Args in matched cmdlines are separated by spaces. Pid only gets matched on script start.')
- parser.add_argument('-s', '--strip',
- metavar='regexp', default=r'^mpv\s+-\s+',
- help='Regexp for bits to strip from produced title (default: %(default)s).'
- ' Can be set to empty string to not strip anything.')
-
- parser.add_argument('-q', '--quiet',
- action='store_true', help='Supress all output from started player pid.')
- parser.add_argument('--debug', action='store_true', help='Verbose operation mode.')
- opts = parser.parse_args(sys.argv[1:] if args is None else args)
-
- global log, err_hook, devnull
- import logging
- logging.basicConfig(level=logging.DEBUG if opts.debug else logging.WARNING)
- log = logging.getLogger()
- err_hook = devnull = None
-
- try: return track_dump_loop(opts)
- finally:
- if err_hook is not None: err_hook()
-
-if __name__ == '__main__': sys.exit(main())
diff --git a/desktop/media/parec_from_flash b/desktop/media/parec_from_flash
deleted file mode 100755
index 5fa9366..0000000
--- a/desktop/media/parec_from_flash
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/bin/bash
-
-
-### Process CLI
-
-idx= output= output_opts=() sink=wiretap
-
-while [[ -n "$1" ]]; do
- case "$1" in
- -h|--help) echo "Usage: $0 $(awk '
- func print_arg() {
- if (!ac) ac=""; else ac=sprintf(" ...(%s)", ac)
- if (ap) printf("[ %s%s ] ", ap, ac) }
- /^\s+case\>/ {parse=1; next}
- /^\s+esac\>/ {print_arg(); exit}
- !parse {next}
- match($0, /^\s*([\-|a-z]+)\)/, a) { print_arg(); ap=a[1]; ac=0 }
- !match(ap,/\<-h|--help\>/)\
- {for (i=1;i/)) ac++}'\
- $0)"
- exit 0 ;;
- -d|--debug) set -x ;;
- -s|--sink-name) shift; sink=$1 ;;
- -o|--output) shift; output=$1 ;;
- -i|--index) shift; idx=( $1 ) ;;
- --) output_opts=true
- [[ -z "$output" ]] && { echo >&2 "Need --output set to use oggenc"; exit 1; } ;;
- *)
- [[ -n "$output_opts" ]] && { output_opts=( "$@" ); break; }
- echo "Unknown arg/option: $1" && exit 1 ;;
- esac
- shift
-done
-
-
-### Try to auto-guess index
-
-if [[ -z "$idx" ]]; then
- idx=( $(
- pacmd list-sink-inputs |
- awk '
- match($0,/^\s*index: ([0-9]+)\s*$/,a) {idx=a[1]}
- idx!="" && $1=="application.process.binary" &&\
- match($0,/\/) {print idx}' ) )
- [[ -z "$idx" ]] && {
- echo >&2\
- 'Failed to get stream index from "pacmd list-sink-inputs",'\
- ' specify it on commandline or something'
- exit 1
- }
-fi
-
-
-### Find existing sink with that name, or create one
-
-sink_idx=$(
- pacmd list-sinks |
- awk '
- match($0,/^\s*index: ([0-9]+)\s*$/,a)\
- {if (parse) print idx; parse=1}
- parse && $1=="name:" &&\
- !match($2,/<\<'"$sink"'\>>/) {parse=0}
- parse && $1=="module:" {idx=$2}
- END {if (parse) print idx}' )
-
-[[ -z "$sink_idx" ]] && sink_idx=$(
- pactl load-module module-null-sink sink_name="$sink" )
-[[ -z "$sink_idx" ]] && { echo >&2 'Failed to get/open sink'; exit 1; }
-
-trap "pactl unload-module $sink_idx" EXIT
-
-for i in "${idx[@]}"
-do pactl move-sink-input "$i" "$sink"
-done
-
-if [[ -z "$output" ]]
-then parec -d "$sink".monitor
-else parec -d "$sink".monitor | oggenc -o "$output" --raw "${output_opts[@]}" -
-fi
diff --git a/desktop/media/radio b/desktop/media/radio
deleted file mode 100755
index 44c63be..0000000
--- a/desktop/media/radio
+++ /dev/null
@@ -1,171 +0,0 @@
-#!/usr/bin/env python2
-# -*- coding: utf-8 -*-
-from __future__ import print_function
-
-import itertools as it, operator as op, functools as ft
-from tempfile import NamedTemporaryFile
-import os, sys, io, re, requests, yaml, subprocess, signal, logging
-
-
-player_wrap = 'media.mpv_icy_track_history' # should be in the sampe repo
-tracks_global = os.path.expanduser('~/tracks.txt')
-
-# --no-cache to prevent any audio/meta de-sync, so mute would work nicely
-# Alternative: '--cache=1024', '--cache-initial=0'
-mpv_args_base = ['--shuffle', '--loop=inf', '--no-cache', '--vo=null']
-
-# radios_conf example (yaml):
-# rmnrelax:
-# url_pls: 'http://www.rmnrelax.de/aacplus/rmnrelax.m3u'
-# args_wrapper: ['--mute-regexp=^\[RMN\]', '--recode-from=latin-1']
-# etnfm:
-# url_pls: 'http://ch1relay1.etn.fm:8100/listen.pls?sid=1'
-# soma:
-# template: true
-# url_pls: 'http://somafm.com/{}.pls'
-# difm:
-# # use as e.g. "difm.vocaltrance" (to template "vocaltrance" into urls)
-# template: true
-# #url_pls: 'http://listen.di.fm/public3/{}.pls?3e4c569'
-# url: 'http://pub5.di.fm:80/di_{}?3e4c569'
-# args_wrapper:
-# - >
-# --mute-regexp=^(There|More of the show after these messages|Choose
-# premium for the best audio experience|Digitally Imported TSTAG_60 ADWTAG|Job
-# Opportunity at DI!.*|There's more to Digitally Imported!)$
-# - --mute-title-prefix
-radios_conf_default = '~/.radio.yaml'
-
-
-_notify_init = False
-def notify(title, body, critical=False, timeout=None, icon=None, fork=True):
- global _notify_init
- if fork and os.fork(): return
- try:
- import gi
- gi.require_version('Notify', '0.7')
- from gi.repository import Notify
- if not _notify_init:
- Notify.init('radio')
- _notify_init = True
- note = Notify.Notification.new(summary=title, body=body, icon=icon)
- if critical: note.set_urgency(Notify.Urgency.CRITICAL)
- if timeout is not None: note.set_timeout(timeout)
- note.show()
- except: pass
- if fork: os._exit(0)
-
-def no_such_chan(err, chans):
- p = ft.partial(print, file=sys.stderr)
- p(err)
- p('\nPossible matches:')
- for chan in chans: p(' ' + chan)
- sys.exit(1)
-
-def get_url(chan, pls=False):
- url = chan.get('url' if not pls else 'url_pls')
- if url and chan.get('template'):
- assert re.search(r'\{(\d+(:.*)?)?\}', url), url
- name = chan['name'].split('.', 1)[-1]
- url = url.format(name)
- return url
-
-def main(args=None):
- import argparse
- parser = argparse.ArgumentParser(description='Start radio playback.')
- parser.add_argument('chan',
- help='Radio channel (exact name or unique part of it) to tune into.')
- parser.add_argument('-c', '--conf',
- metavar='path', default=radios_conf_default,
- help='Path to the configuration file with a list of radios.'
- ' See head of this script for an example. Default: %(default)s')
- parser.add_argument('--desktop-notification-props',
- default='{fork: false, icon: amarok, critical: true}', metavar='yaml_data',
- help='Desktop notification properties (keywords) to pass to notify() function (see code).'
- ' "false" (bool value) can be passed here to disable desktop notifications entirely.'
- ' Default: %(default)s')
- parser.add_argument('--debug', action='store_true', help='Verbose operation mode.')
- opts = parser.parse_args(sys.argv[1:] if args is None else args)
-
- logging.basicConfig(level=logging.DEBUG if opts.debug else logging.WARNING)
- log = logging.getLogger('radio')
-
- files_cleanup = set()
- radios = os.path.expanduser(opts.conf)
- with io.open(radios) as src: radios = yaml.safe_load(src)
-
- chan = chan_name = opts.chan
- if chan not in radios:
- match = list(k for k in radios if k.startswith(chan))
- if not match:
- chan_tpl_base = chan.split('.', 1)[0]
- match = radios.get(chan_tpl_base)
- if match: match = match.get('template') and [chan_tpl_base]
- if not match: match = list(k for k in radios if chan in k)
- if not match: no_such_chan('Unable to match {!r}'.format(chan), radios.keys())
- if len(match) > 1: no_such_chan('Unable to uniquely match {!r}'.format(chan), match)
- chan, = match
-
- log.debug('Using chan: %s', chan)
- chan = radios[chan]
- chan.update(name=chan_name)
-
- url = get_url(chan, pls=True)
- if url:
- r = requests.get(url)
- r.raise_for_status()
- pls = NamedTemporaryFile(prefix='radio.pls.') # will be cleaned-up by child pid
- files_cleanup.add(pls.name)
- pls.write(r.text)
- pls.flush()
- url = '--playlist={}'.format(pls.name)
- else:
- url = get_url(chan)
- assert url, chan
-
- tracks = tracks_global
- if 'tracks' in chan: tracks = chan['tracks']
- if not os.path.exists(tracks):
- with open(tracks, 'ab'): pass
-
- notify_kws = yaml.safe_load(opts.desktop_notification_props)
- if notify_kws is not False: notify_kws = notify_kws or dict()
-
- pid_player = os.getpid()
- pid = os.fork()
- if not pid: ### child
- os.setsid()
-
- tail = subprocess.Popen(['tail', '-n1', '-f', tracks], stdout=subprocess.PIPE)
-
- def player_check(sig=None, frm=None):
- try: os.kill(pid_player, 0)
- except OSError:
- tail.terminate()
- sys.exit(0)
- signal.signal(signal.SIGALRM, player_check)
- signal.alarm(5 *60)
-
- for line in iter(tail.stdout.readline, ''):
- player_check()
- line = line.strip()
- if notify_kws is not False:
- notify('mpv: radio {}'.format(chan['name']), line, **notify_kws)
- print(line)
-
- sys.exit(0)
-
- else: ### parent (player)
- args_wrapper, args = chan.get('args_wrapper', list()), chan.get('args', list())
- args = mpv_args_base + args
-
- if opts.debug: args_wrapper.extend(['--debug', '--passthrough'])
- if tracks: args_wrapper.extend([ '-d', tracks, '-t',
- '--line-format', '{{ts}}{{mute}}{{}} [{}]\n'.format(chan['name']) ])
- args.append(url)
-
- args = [player_wrap] + args_wrapper + ['--'] + args
- log.debug('Running: %s', ' '.join(args))
- os.execvp(player_wrap, args)
-
-if __name__ == '__main__': sys.exit(main())