Compare commits

..

No commits in common. "8182cf2e07799d462dd5584dfa49c145cf160fbc" and "c65e34b283545df606c730ad59d91dc02ee46905" have entirely different histories.

3 changed files with 38 additions and 106 deletions

6
README
View file

@ -8,8 +8,6 @@ Installation
depending on which platform you are using this program, e.g.: depending on which platform you are using this program, e.g.:
* Linux: $HOME/.config/radiorec/settings.ini or * Linux: $HOME/.config/radiorec/settings.ini or
* Windows: %LOCALAPPDATA%/radiorec/settings.ini * Windows: %LOCALAPPDATA%/radiorec/settings.ini
or use the commandline option '-s' to specify the location of the
settings.ini file.
- Adjust the settings to your needs. You can happily add more radio stations - Adjust the settings to your needs. You can happily add more radio stations
to the STATIONS section. to the STATIONS section.
!!! Check at least the the target_dir !!! !!! Check at least the the target_dir !!!
@ -56,8 +54,8 @@ how to schedule your recording tasks.
Known problems Known problems
============== ==============
The Windows command line (cmd and powershell) still has problems with UTF-8. The Windows command line (cmd and powershell) still has problems with UTF-8.
Using the --verbose option might cause the script to crash with an Using the --verbose option might cause the script to crash with an
UnicodeEncodeError. If you want to avoid the crash, you have to do both, UnicodeEncodeError. If you want to avoid the crash, you have to do both,
change the command line codepage and the font. For example, doing a change the command line codepage and the font. For example, doing a
"chcp 65001" and changing the font to "Lucidia Console" should help. "chcp 65001" and changing the font to "Lucidia Console" should help.

View file

@ -26,15 +26,7 @@ import os
import stat import stat
import sys import sys
import threading import threading
import urllib3 import urllib.request
import logging
import time
from urllib3.exceptions import MaxRetryError
logging.basicConfig(level=logging.DEBUG)
def print_time():
return time.strftime("%Y-%m-%d %H:%M:%S")
def check_duration(value): def check_duration(value):
@ -42,57 +34,45 @@ def check_duration(value):
value = int(value) value = int(value)
except ValueError: except ValueError:
raise argparse.ArgumentTypeError( raise argparse.ArgumentTypeError(
'Duration in minutes must be a positive integer.') 'Duration must be a positive integer.')
if value < 1: if value < 1:
raise argparse.ArgumentTypeError( raise argparse.ArgumentTypeError(
'Duration in minutes must be a positive integer.') 'Duration must be a positive integer.')
else: else:
return value return value
def read_settings(args): def read_settings():
settings_base_dir = '' settings_base_dir = ''
if args.settings: if sys.platform.startswith('linux'):
settings_base_dir = args.settings
elif sys.platform.startswith('linux'):
settings_base_dir = os.getenv( settings_base_dir = os.getenv(
'HOME') + os.sep + '.config' + os.sep + 'radiorec' 'HOME') + os.sep + '.config' + os.sep + 'radiorec'
elif sys.platform == 'win32': elif sys.platform == 'win32':
settings_base_dir = os.getenv('LOCALAPPDATA') + os.sep + 'radiorec' settings_base_dir = os.getenv('LOCALAPPDATA') + os.sep + 'radiorec'
elif sys.platform == 'darwin': elif sys.platform == 'darwin':
settings_base_dir = os.getenv('HOME') + os.sep + 'Library' + os.sep + \ settings_base_dir = os.getenv('HOME') + os.sep + 'Library' + os.sep + 'Application Support' + os.sep + 'radiorec'
'Application Support' + os.sep + 'radiorec'
settings_base_dir += os.sep settings_base_dir += os.sep
config = configparser.ConfigParser() config = configparser.ConfigParser()
try: try:
config.read_file(open(settings_base_dir + 'settings.ini')) config.read_file(open(settings_base_dir + 'settings.ini'))
except FileNotFoundError as err: except FileNotFoundError as err:
print(str(err)) print(str(err))
print('Please copy/create the settings file to/in the appropriate location.') print('Please copy/create the settings file to/in the appropriate '
'location.')
sys.exit() sys.exit()
return dict(config.items()) return dict(config.items())
def record_worker(stoprec, streamurl, target_dir, args): def record_worker(stoprec, streamurl, target_dir, args):
pool = urllib3.PoolManager() conn = urllib.request.urlopen(streamurl)
conn = pool.request('GET', streamurl, preload_content=False)
conn.auto_close = False
if conn.status != 200:
conn.release_conn()
time.sleep(10)
verboseprint(print_time() + " ... Waited to return for retry bcof status " + str(conn.status))
return
cur_dt_string = datetime.datetime.now().strftime('%Y-%m-%dT%H_%M_%S') cur_dt_string = datetime.datetime.now().strftime('%Y-%m-%dT%H_%M_%S')
filename = target_dir + os.sep + cur_dt_string + "_" + args.station filename = target_dir + os.sep + cur_dt_string + "_" + args.station
if args.name: if args.name:
filename += '_' + args.name filename += '_' + args.name
content_type = conn.getheader('Content-Type') content_type = conn.getheader('Content-Type')
if (content_type == 'audio/mpeg'): if(content_type == 'audio/mpeg'):
filename += '.mp3' filename += '.mp3'
elif(content_type == 'application/aacp' or content_type == 'audio/aacp'):
filename += '.aac'
elif(content_type == 'application/ogg' or content_type == 'audio/ogg'): elif(content_type == 'application/ogg' or content_type == 'audio/ogg'):
filename += '.ogg' filename += '.ogg'
elif(content_type == 'audio/x-mpegurl'): elif(content_type == 'audio/x-mpegurl'):
@ -102,22 +82,19 @@ def record_worker(stoprec, streamurl, target_dir, args):
print('Unknown content type "' + content_type + '". Assuming mp3.') print('Unknown content type "' + content_type + '". Assuming mp3.')
filename += '.mp3' filename += '.mp3'
verboseprint(print_time() + " ... Writing to: " + filename + ", Content-Type: " + conn.getheader('Content-Type')) with open(filename, "wb") as target:
with open(filename, 'wb') as target:
if args.public: if args.public:
verboseprint('Apply public write permissions (Linux only)') verboseprint('Apply public write permissions (Linux only)')
os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH) os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP |
stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
verboseprint('Recording ' + args.station + '...')
while(not stoprec.is_set() and not conn.closed): while(not stoprec.is_set() and not conn.closed):
target.write(conn.read(1024)) target.write(conn.read(1024))
verboseprint(print_time() + " ... Connection closed = " + str(conn.closed))
conn.release_conn()
def record(args): def record(args):
settings = read_settings(args) settings = read_settings()
streamurl = '' streamurl = ''
tmpstr = ''
global verboseprint global verboseprint
verboseprint = print if args.verbose else lambda *a, **k: None verboseprint = print if args.verbose else lambda *a, **k: None
@ -128,72 +105,37 @@ def record(args):
sys.exit() sys.exit()
if streamurl.endswith('.m3u'): if streamurl.endswith('.m3u'):
verboseprint('Seems to be an M3U playlist. Trying to parse...') verboseprint('Seems to be an M3U playlist. Trying to parse...')
pool = urllib3.PoolManager() with urllib.request.urlopen(streamurl) as remotefile:
try: for line in remotefile:
remotefile = pool.request('GET', streamurl) if not line.decode('utf-8').startswith('#') and len(line) > 1:
except MaxRetryError: tmpstr = line.decode('utf-8')
logging.getLogger(__name__).error('The URL of the station is somehow faulty! Check' +
args.station + ' in the Settings!')
sys.exit()
if remotefile.status != 200:
logging.getLogger(__name__).error(
'The URL of the station is somehow faulty! Check' + args.station + ' in the Settings!')
sys.exit(1)
else:
for line in remotefile.data.decode().split():
if not line.startswith('#') and len(line) > 1 and line.endswith('mp3'):
tmpstr = line
break break
if not tmpstr: streamurl = tmpstr
logging.getLogger(__name__).error('Could not find a mp3 stream') verboseprint('stream url: ' + streamurl)
sys.exit(1)
else:
streamurl = tmpstr
verboseprint(print_time() + " ... Stream URL: " + streamurl)
target_dir = os.path.expandvars(settings['GLOBAL']['target_dir']) target_dir = os.path.expandvars(settings['GLOBAL']['target_dir'])
if not os.path.isdir(target_dir): stoprec = threading.Event()
try:
os.mkdir(target_dir)
except FileNotFoundError:
logging.getLogger(__name__).error('Target directory not found! Check that ' +
target_dir + ' is a valid folder!')
sys.exit(1)
started_at = time.time()
should_end_at = started_at + (args.duration * 60)
remaining = (args.duration * 60)
# as long as recording is supposed to run recthread = threading.Thread(target=record_worker,
while time.time() < should_end_at: args=(stoprec, streamurl, target_dir, args))
stoprec = threading.Event() recthread.setDaemon(True)
recthread = threading.Thread(target=record_worker, args=(stoprec, streamurl, target_dir, args)) recthread.start()
recthread.setDaemon(True) recthread.join(args.duration * 60)
recthread.start()
verboseprint(print_time() + " ... Started thread " + str(recthread) + " timeout: " +
str(remaining / 60) + " min")
recthread.join(remaining)
verboseprint(print_time() + " ... Came out of rec thread again")
if recthread.is_alive: if(recthread.is_alive):
stoprec.set() stoprec.set()
verboseprint(print_time() + " ... Called stoprec.set()")
else:
verboseprint(print_time() + " ... recthread.is_alive = False")
remaining = should_end_at - time.time()
verboseprint(print_time() + " ... Remaining: " + str(remaining / 60) +
", Threads: " + str(threading.activeCount()))
def list(args): def list(args):
settings = read_settings(args) settings = read_settings()
for key in sorted(settings['STATIONS']): for key in sorted(settings['STATIONS']):
print(key) print(key)
def main(): def main():
parser = argparse.ArgumentParser(description='This program records internet radio streams. ' parser = argparse.ArgumentParser(description='This program records '
'It is free software and comes with ABSOLUTELY NO WARRANTY.') 'internet radio streams. It is free '
'software and comes with ABSOLUTELY NO '
'WARRANTY.')
subparsers = parser.add_subparsers(help='sub-command help') subparsers = parser.add_subparsers(help='sub-command help')
parser_record = subparsers.add_parser('record', help='Record a station') parser_record = subparsers.add_parser('record', help='Record a station')
parser_record.add_argument('station', type=str, parser_record.add_argument('station', type=str,
@ -208,14 +150,9 @@ def main():
help="Public write permissions (Linux only)") help="Public write permissions (Linux only)")
parser_record.add_argument( parser_record.add_argument(
'-v', '--verbose', action='store_true', help="Verbose output") '-v', '--verbose', action='store_true', help="Verbose output")
parser_record.add_argument(
'-s', '--settings', nargs='?', type=str,
help="specify alternative location for settings.ini")
parser_record.set_defaults(func=record) parser_record.set_defaults(func=record)
parser_list = subparsers.add_parser('list', help='List all known stations') parser_list = subparsers.add_parser('list', help='List all known stations')
parser_list.set_defaults(func=list) parser_list.set_defaults(func=list)
parser_list.add_argument('-s', '--settings', nargs='?', type=str,
help="specify alternative location for settings.ini")
if not len(sys.argv) > 1: if not len(sys.argv) > 1:
print('Error: No argument specified.\n') print('Error: No argument specified.\n')
@ -224,6 +161,5 @@ def main():
args = parser.parse_args() args = parser.parse_args()
args.func(args) args.func(args)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -1,15 +1,13 @@
[GLOBAL] [GLOBAL]
target_dir = $HOME/Arbeitsfläche target_dir = $HOME/Arbeitsfläche
[STATIONS] [STATIONS]
brklassik = http://streams.br-online.de/br-klassik_2.m3u brklassik = http://streams.br-online.de/br-klassik_2.m3u
dkultur = http://www.deutschlandradio.de/streaming/dkultur.m3u dkultur = http://www.deutschlandradio.de/streaming/dkultur.m3u
dlf = http://www.deutschlandradio.de/streaming/dlf.m3u dlf = http://www.deutschlandradio.de/streaming/dlf.m3u
dwissen = http://dradio_mp3_dwissen_m.akacast.akamaistream.net/7/728/142684/v1/gnl.akacast.akamaistream.net/dradio_mp3_dwissen_m dwissen = http://dradio_mp3_dwissen_m.akacast.akamaistream.net/7/728/142684/v1/gnl.akacast.akamaistream.net/dradio_mp3_dwissen_m
erfplus = http://c14000-l.i.core.cdn.streamfarm.net/14000cina/live/3212erf_96/live_de_96.mp3 erfplus = http://c14000-l.i.core.cdn.streamfarm.net/14000cina/live/3212erf_96/live_de_96.mp3
mdrklassik = http://avw.mdr.de/streams/284350-0_mp3_high.m3u mdrklassik = http://avw.mdr.de/livestreams/mdr_klassik_live_128.m3u
radioeins = http://www.radioeins.de/live.m3u radioeins = http://www.radioeins.de/live.m3u
swr2 = http://mp3-live.swr.de/swr2_m.m3u swr2 = http://mp3-live.swr.de/swr2_m.m3u
wdr3 = http://wdr-wdr3-live.icecast.wdr.de/wdr/wdr3/live/mp3/256/stream.mp3 wdr3 = http://www.wdr.de/wdrlive/media/mp3/wdr3_hq.m3u
ndrkultur = http://www.ndr.de/resources/metadaten/audio/m3u/ndrkultur.m3u