batchmp 1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- batchmp/__init__.py +0 -0
- batchmp/cli/__init__.py +0 -0
- batchmp/cli/base/__init__.py +0 -0
- batchmp/cli/base/bmp_dispatch.py +60 -0
- batchmp/cli/base/bmp_options.py +349 -0
- batchmp/cli/base/vchk.py +47 -0
- batchmp/cli/bmfp/__init__.py +0 -0
- batchmp/cli/bmfp/bmfp_dispatch.py +120 -0
- batchmp/cli/bmfp/bmfp_options.py +442 -0
- batchmp/cli/renamer/__init__.py +0 -0
- batchmp/cli/renamer/renamer_dispatch.py +135 -0
- batchmp/cli/renamer/renamer_options.py +355 -0
- batchmp/cli/tagger/__init__.py +0 -0
- batchmp/cli/tagger/tagger_dispatch.py +143 -0
- batchmp/cli/tagger/tagger_options.py +338 -0
- batchmp/commons/__init__.py +0 -0
- batchmp/commons/chainedhandler.py +102 -0
- batchmp/commons/descriptors.py +173 -0
- batchmp/commons/progressbar.py +154 -0
- batchmp/commons/taskprocessor.py +149 -0
- batchmp/commons/utils.py +194 -0
- batchmp/ffmptools/__init__.py +0 -0
- batchmp/ffmptools/ffcommands/__init__.py +0 -0
- batchmp/ffmptools/ffcommands/cmdopt.py +115 -0
- batchmp/ffmptools/ffcommands/convert.py +130 -0
- batchmp/ffmptools/ffcommands/cuesplit.py +223 -0
- batchmp/ffmptools/ffcommands/denoise.py +173 -0
- batchmp/ffmptools/ffcommands/fragment.py +121 -0
- batchmp/ffmptools/ffcommands/normalize_peak.py +135 -0
- batchmp/ffmptools/ffcommands/segment.py +157 -0
- batchmp/ffmptools/ffcommands/silencesplit.py +159 -0
- batchmp/ffmptools/ffrunner.py +189 -0
- batchmp/ffmptools/ffutils.py +300 -0
- batchmp/ffmptools/processors/__init__.py +0 -0
- batchmp/ffmptools/processors/basefp.py +92 -0
- batchmp/ffmptools/processors/ffentry.py +81 -0
- batchmp/ffmptools/utils/__init__.py +0 -0
- batchmp/ffmptools/utils/cueparse.py +227 -0
- batchmp/ffmptools/utils/cuesheet.py +239 -0
- batchmp/fstools/__init__.py +0 -0
- batchmp/fstools/builders/__init__.py +0 -0
- batchmp/fstools/builders/fsb.py +221 -0
- batchmp/fstools/builders/fsentry.py +60 -0
- batchmp/fstools/builders/fsprms.py +372 -0
- batchmp/fstools/dirtools.py +549 -0
- batchmp/fstools/fsutils.py +272 -0
- batchmp/fstools/rename.py +390 -0
- batchmp/fstools/walker.py +79 -0
- batchmp/tags/__init__.py +0 -0
- batchmp/tags/handlers/__init__.py +0 -0
- batchmp/tags/handlers/basehandler.py +99 -0
- batchmp/tags/handlers/ffmphandler.py +75 -0
- batchmp/tags/handlers/ffmphandlers/__init__.py +0 -0
- batchmp/tags/handlers/ffmphandlers/base.py +243 -0
- batchmp/tags/handlers/mtghandler.py +56 -0
- batchmp/tags/handlers/pmhandler.py +36 -0
- batchmp/tags/handlers/tagsholder.py +264 -0
- batchmp/tags/output/__init__.py +0 -0
- batchmp/tags/output/formatters.py +218 -0
- batchmp/tags/processors/__init__.py +0 -0
- batchmp/tags/processors/basetp.py +266 -0
- batchmp-1.4.dist-info/METADATA +422 -0
- batchmp-1.4.dist-info/RECORD +68 -0
- batchmp-1.4.dist-info/WHEEL +5 -0
- batchmp-1.4.dist-info/entry_points.txt +5 -0
- batchmp-1.4.dist-info/licenses/LICENSE +11 -0
- batchmp-1.4.dist-info/top_level.txt +1 -0
- batchmp-1.4.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# coding=utf8
|
|
2
|
+
## Copyright (c) 2014 Arseniy Kuznetsov
|
|
3
|
+
##
|
|
4
|
+
## This program is free software; you can redistribute it and/or
|
|
5
|
+
## modify it under the terms of the GNU General Public License
|
|
6
|
+
## as published by the Free Software Foundation; either version 2
|
|
7
|
+
## of the License, or (at your option) any later version.
|
|
8
|
+
##
|
|
9
|
+
## This program is distributed in the hope that it will be useful,
|
|
10
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
## GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
''' A simple single-line console progress bar
|
|
16
|
+
Displays progress by fractions of 10%
|
|
17
|
+
Supports premature stops & info messages during execution
|
|
18
|
+
'''
|
|
19
|
+
import sys, time
|
|
20
|
+
from bisect import bisect as bs
|
|
21
|
+
import threading, queue
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CmdProgressBarRefreshRate:
|
|
26
|
+
SLOW = 0.2
|
|
27
|
+
MODERATE = 0.1
|
|
28
|
+
FAST = 0.02
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@contextmanager
|
|
32
|
+
def progress_bar(starts_from = 0, refresh_rate = CmdProgressBarRefreshRate.MODERATE):
|
|
33
|
+
''' Enables usage via a runtime context
|
|
34
|
+
'''
|
|
35
|
+
p_bar = CmdProgressBar(starts_from, refresh_rate)
|
|
36
|
+
p_bar.start()
|
|
37
|
+
try:
|
|
38
|
+
yield p_bar
|
|
39
|
+
finally:
|
|
40
|
+
p_bar.stop()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CmdProgressBarUpdateTypes:
|
|
44
|
+
UPDATE_PROGRESS = 0
|
|
45
|
+
UPDATE_MSG = 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CmdProgressBar(object):
|
|
49
|
+
def __init__(self, start_from=0, refresh_rate = CmdProgressBarRefreshRate.MODERATE):
|
|
50
|
+
self._queue = queue.Queue(1) # used to communicate with the worker thread
|
|
51
|
+
self._end_event = threading.Event() # used to exit
|
|
52
|
+
|
|
53
|
+
self.progress = start_from # access via property, for validation $ enqueuing
|
|
54
|
+
self._info_msg = None
|
|
55
|
+
|
|
56
|
+
self._bar_thread = threading.Thread(target=self._show_progress,
|
|
57
|
+
args=(start_from, refresh_rate, self._end_event, self._queue,))
|
|
58
|
+
self._bar_thread.daemon = True
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def info_msg(self):
|
|
62
|
+
return self._info_msg
|
|
63
|
+
@info_msg.setter
|
|
64
|
+
def info_msg(self, value):
|
|
65
|
+
self._queue.put((CmdProgressBarUpdateTypes.UPDATE_MSG, value))
|
|
66
|
+
self._info_msg = value
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def progress(self):
|
|
70
|
+
return self._progress
|
|
71
|
+
@progress.setter
|
|
72
|
+
def progress(self, value):
|
|
73
|
+
# unless in stopping mode, set & enqueue
|
|
74
|
+
if not self._end_event.is_set():
|
|
75
|
+
if value < 0: value = 0
|
|
76
|
+
if value > 100: value = 100
|
|
77
|
+
self._queue.put((CmdProgressBarUpdateTypes.UPDATE_PROGRESS, value))
|
|
78
|
+
self._progress = value
|
|
79
|
+
|
|
80
|
+
# the worker thread method
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _show_progress(last_known_progress, refresh_rate, end_event, queue):
|
|
83
|
+
progress_values = [i for i in range(0, 110, 10)] # [0, 10, ..., 100]
|
|
84
|
+
chars = '|/-\\'
|
|
85
|
+
msg = None
|
|
86
|
+
while True:
|
|
87
|
+
if not queue.full():
|
|
88
|
+
# nothing in the queue yet, keep showing the last known progres
|
|
89
|
+
progress = last_known_progress
|
|
90
|
+
else:
|
|
91
|
+
update = queue.get()
|
|
92
|
+
# figure out what kind of update is being requested
|
|
93
|
+
if update[0] == CmdProgressBarUpdateTypes.UPDATE_PROGRESS:
|
|
94
|
+
progress = update[1]
|
|
95
|
+
last_known_progress = progress
|
|
96
|
+
else:
|
|
97
|
+
msg = update[1]
|
|
98
|
+
# signal that the value has been consumed
|
|
99
|
+
queue.task_done()
|
|
100
|
+
|
|
101
|
+
num_progress_vals = bs(progress_values, progress)
|
|
102
|
+
progress_info = '..'.join([''.join((str(i), '%')) for i in progress_values[:num_progress_vals]])
|
|
103
|
+
progress_info = ''.join((progress_info, '.' * (53 - len(progress_info))))
|
|
104
|
+
|
|
105
|
+
# for info msg updates, display the message
|
|
106
|
+
if msg != None:
|
|
107
|
+
sys.stdout.write(''.join(('\r', ' ' * 70, '\r')))
|
|
108
|
+
sys.stdout.write(''.join((msg, '\n')))
|
|
109
|
+
msg = None
|
|
110
|
+
|
|
111
|
+
# show pogress
|
|
112
|
+
for c in chars:
|
|
113
|
+
sys.stdout.write('\r[ {0} ..{1}.. ]'.format(c, progress_info))
|
|
114
|
+
sys.stdout.flush()
|
|
115
|
+
time.sleep(refresh_rate)
|
|
116
|
+
|
|
117
|
+
if end_event.is_set():
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
# starts the worker thread
|
|
121
|
+
def start(self):
|
|
122
|
+
self._bar_thread.start()
|
|
123
|
+
|
|
124
|
+
# stops the worker thread
|
|
125
|
+
# handles premature exits (e.g., when the method is called at 70% progress)
|
|
126
|
+
def stop(self):
|
|
127
|
+
# check if a graceful stop is needed
|
|
128
|
+
if self._progress < 100:
|
|
129
|
+
# looks like a premature exit,
|
|
130
|
+
# set progress to max
|
|
131
|
+
self.progress = 100
|
|
132
|
+
|
|
133
|
+
# wait till the queue is processed
|
|
134
|
+
self._queue.join()
|
|
135
|
+
|
|
136
|
+
# OK to stop now
|
|
137
|
+
self._end_event.set()
|
|
138
|
+
self._bar_thread.join()
|
|
139
|
+
sys.stdout.write(''.join(('\r', ' ' * 70, '\r')))
|
|
140
|
+
sys.stdout.flush()
|
|
141
|
+
|
|
142
|
+
# Quick Dev Test
|
|
143
|
+
if __name__ == '__main__':
|
|
144
|
+
start_from, msg_target, progress_target = 30, 40, 60
|
|
145
|
+
with progress_bar(start_from) as p_bar:
|
|
146
|
+
while True:
|
|
147
|
+
if p_bar.progress == msg_target:
|
|
148
|
+
p_bar.info_msg = 'At {}%, and doing well'.format(msg_target)
|
|
149
|
+
if p_bar.progress == progress_target:
|
|
150
|
+
p_bar.info_msg = 'At {}%, and feel like finishing early'.format(progress_target)
|
|
151
|
+
break
|
|
152
|
+
p_bar.progress += 10
|
|
153
|
+
print('All Done')
|
|
154
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# coding=utf8
|
|
2
|
+
## Copyright (c) 2014 Arseniy Kuznetsov
|
|
3
|
+
##
|
|
4
|
+
## This program is free software; you can redistribute it and/or
|
|
5
|
+
## modify it under the terms of the GNU General Public License
|
|
6
|
+
## as published by the Free Software Foundation; either version 2
|
|
7
|
+
## of the License, or (at your option) any later version.
|
|
8
|
+
##
|
|
9
|
+
## This program is distributed in the hope that it will be useful,
|
|
10
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
## GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import copyreg, types, multiprocessing
|
|
16
|
+
from abc import ABCMeta, abstractmethod
|
|
17
|
+
from batchmp.commons.progressbar import progress_bar, CmdProgressBarRefreshRate
|
|
18
|
+
from batchmp.commons.utils import timed, MiscHelpers
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Task(metaclass = ABCMeta):
|
|
22
|
+
''' Abstract TasksProcessor task
|
|
23
|
+
'''
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def execute(self):
|
|
26
|
+
return TaskResult()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TaskResult:
|
|
30
|
+
''' TasksProcessor Task result
|
|
31
|
+
'''
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self._task_steps_info_msgs = []
|
|
34
|
+
self._task_steps_durations = []
|
|
35
|
+
self._succeeded = False
|
|
36
|
+
|
|
37
|
+
def add_task_step_duration(self, step_duration):
|
|
38
|
+
self._task_steps_durations.append(step_duration)
|
|
39
|
+
|
|
40
|
+
def add_task_step_info_msg(self, step_info_msg):
|
|
41
|
+
self._task_steps_info_msgs.append(step_info_msg)
|
|
42
|
+
|
|
43
|
+
def add_report_msg(self, processed_fpath):
|
|
44
|
+
task_duration_str = MiscHelpers.time_delta_str(self.task_duration)
|
|
45
|
+
self.add_task_step_info_msg('Done processing\n {0}\n in {1}'.format(
|
|
46
|
+
processed_fpath, task_duration_str))
|
|
47
|
+
@property
|
|
48
|
+
def succeeded(self):
|
|
49
|
+
return self._succeeded
|
|
50
|
+
@succeeded.setter
|
|
51
|
+
def succeeded(self, value):
|
|
52
|
+
self._succeeded = value
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def task_output(self):
|
|
56
|
+
task_output = None
|
|
57
|
+
for info_msg in self._task_steps_info_msgs:
|
|
58
|
+
task_output = '{0}{1}{2}'.format(task_output if task_output else '',
|
|
59
|
+
'\n' if task_output else '',
|
|
60
|
+
info_msg)
|
|
61
|
+
return task_output
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def task_duration(self):
|
|
65
|
+
task_duration = 0.0
|
|
66
|
+
for step_duration in self._task_steps_durations:
|
|
67
|
+
task_duration += step_duration
|
|
68
|
+
return task_duration
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TasksProcessor:
|
|
72
|
+
''' Runs cmd-line Tasks, sequentially or in a pool of processes
|
|
73
|
+
Displays progress / tasks done
|
|
74
|
+
'''
|
|
75
|
+
def _process_task(self, task):
|
|
76
|
+
result = task.execute()
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
@timed
|
|
80
|
+
def process_tasks(self, tasks_queue, serial_exec = False, num_workers = None, quiet = False):
|
|
81
|
+
tasks_results = []
|
|
82
|
+
cpu_core_time = 0.0
|
|
83
|
+
|
|
84
|
+
num_tasks = len(tasks_queue)
|
|
85
|
+
serial_exec = serial_exec or num_tasks == 1
|
|
86
|
+
if num_tasks > 0:
|
|
87
|
+
# Pre-processing msgs
|
|
88
|
+
if serial_exec:
|
|
89
|
+
print('Processing {0} {1}'.format(num_tasks,
|
|
90
|
+
'task' if num_tasks == 1 else 'tasks sequentially'))
|
|
91
|
+
else:
|
|
92
|
+
if not num_workers:
|
|
93
|
+
num_workers = multiprocessing.cpu_count()
|
|
94
|
+
print('Processing {0} tasks with pool of {1} worker processes'.format(num_tasks, num_workers))
|
|
95
|
+
|
|
96
|
+
# start showing progress
|
|
97
|
+
with progress_bar(refresh_rate = CmdProgressBarRefreshRate.MODERATE) as p_bar:
|
|
98
|
+
def _make_progress(result):
|
|
99
|
+
nonlocal cpu_core_time
|
|
100
|
+
tasks_results.append(result)
|
|
101
|
+
if not quiet:
|
|
102
|
+
p_bar.info_msg = result.task_output
|
|
103
|
+
cpu_core_time += result.task_duration
|
|
104
|
+
p_bar.progress = len(tasks_results) / num_tasks * 100
|
|
105
|
+
|
|
106
|
+
if serial_exec:
|
|
107
|
+
# just loop through the queue of tasks
|
|
108
|
+
for task in tasks_queue:
|
|
109
|
+
result = self._process_task(task)
|
|
110
|
+
_make_progress(result)
|
|
111
|
+
else:
|
|
112
|
+
# init the pool and kick it off
|
|
113
|
+
with multiprocessing.Pool(num_workers) as pool:
|
|
114
|
+
for result in pool.imap_unordered(self._process_task, tasks_queue):
|
|
115
|
+
_make_progress(result)
|
|
116
|
+
|
|
117
|
+
# return tasks results, aggregate CPU cores time, and total time elapsed (via @timed)
|
|
118
|
+
return tasks_results, cpu_core_time
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
Python multiprocessing pickles stuff, and bound methods are not picklable
|
|
123
|
+
The code below serves as a workaround, more at:
|
|
124
|
+
http://bytes.com/topic/python/answers/552476-why-cant-you-pickle-instancemethods#edit2155350
|
|
125
|
+
"""
|
|
126
|
+
def _pickle_method(method):
|
|
127
|
+
func_name = method.im_func.__name__
|
|
128
|
+
obj = method.im_self
|
|
129
|
+
cls = method.im_class
|
|
130
|
+
|
|
131
|
+
if func_name.startswith('__') and not func_name.endswith('__'):
|
|
132
|
+
cls_name = cls.__name__.lstrip('_')
|
|
133
|
+
if cls_name:
|
|
134
|
+
func_name = '_' + cls_name + func_name
|
|
135
|
+
|
|
136
|
+
return _unpickle_method, (func_name, obj, cls)
|
|
137
|
+
|
|
138
|
+
def _unpickle_method(func_name, obj, cls):
|
|
139
|
+
for cls in cls.mro():
|
|
140
|
+
try:
|
|
141
|
+
func = cls.__dict__[func_name]
|
|
142
|
+
except KeyError:
|
|
143
|
+
pass
|
|
144
|
+
else:
|
|
145
|
+
break
|
|
146
|
+
return func.__get__(obj, cls)
|
|
147
|
+
|
|
148
|
+
copyreg.pickle(types.MethodType, _pickle_method, _unpickle_method)
|
|
149
|
+
|
batchmp/commons/utils.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# coding=utf8
|
|
2
|
+
## Copyright (c) 2014 Arseniy Kuznetsov
|
|
3
|
+
##
|
|
4
|
+
## This program is free software; you can redistribute it and/or
|
|
5
|
+
## modify it under the terms of the GNU General Public License
|
|
6
|
+
## as published by the Free Software Foundation; either version 2
|
|
7
|
+
## of the License, or (at your option) any later version.
|
|
8
|
+
##
|
|
9
|
+
## This program is distributed in the hope that it will be useful,
|
|
10
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
## GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
import subprocess, shlex, time, tempfile, shutil
|
|
15
|
+
import datetime, math, functools
|
|
16
|
+
from functools import wraps
|
|
17
|
+
from urllib.parse import urlparse
|
|
18
|
+
import urllib.request, urllib.error
|
|
19
|
+
from contextlib import contextmanager
|
|
20
|
+
|
|
21
|
+
''' General-level utilities
|
|
22
|
+
'''
|
|
23
|
+
|
|
24
|
+
@contextmanager
|
|
25
|
+
def temp_dir():
|
|
26
|
+
''' Temp dir context manager
|
|
27
|
+
'''
|
|
28
|
+
tmp_dir = tempfile.mkdtemp()
|
|
29
|
+
try:
|
|
30
|
+
yield tmp_dir
|
|
31
|
+
finally:
|
|
32
|
+
# remove tmp dir
|
|
33
|
+
shutil.rmtree(tmp_dir)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def timed(f):
|
|
37
|
+
""" A timing decorator
|
|
38
|
+
"""
|
|
39
|
+
@wraps(f)
|
|
40
|
+
def wrapper(*args, **kwds):
|
|
41
|
+
start = time.time()
|
|
42
|
+
result = f(*args, **kwds)
|
|
43
|
+
elapsed = time.time() - start
|
|
44
|
+
return (result, elapsed)
|
|
45
|
+
return wrapper
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CmdProcessingError(Exception):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@timed
|
|
52
|
+
def run_cmd(cmd, shell = False):
|
|
53
|
+
''' Runs shell commands in a separate process
|
|
54
|
+
'''
|
|
55
|
+
if not shell:
|
|
56
|
+
cmd = shlex.split(cmd)
|
|
57
|
+
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell = shell)
|
|
58
|
+
output = proc.communicate()[0].decode('utf-8')
|
|
59
|
+
if proc.returncode != 0:
|
|
60
|
+
raise CmdProcessingError(output)
|
|
61
|
+
return output
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MiscHelpers:
|
|
65
|
+
@staticmethod
|
|
66
|
+
def int_num_digits(num):
|
|
67
|
+
''' Number of digits in an int number
|
|
68
|
+
'''
|
|
69
|
+
if not isinstance(num, int):
|
|
70
|
+
try:
|
|
71
|
+
num = int(num)
|
|
72
|
+
except ValueError as e:
|
|
73
|
+
return 0
|
|
74
|
+
num_digits = 1
|
|
75
|
+
while (int(abs(num)/(10**num_digits)) > 0):
|
|
76
|
+
num_digits += 1
|
|
77
|
+
return num_digits
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def time_delta(td_str):
|
|
81
|
+
''' Timedelta from the "hh:mm:ss[.xxx]" format
|
|
82
|
+
'''
|
|
83
|
+
hrs = mins = secs = None
|
|
84
|
+
td = td_str.split(':')
|
|
85
|
+
time_parts = range(len(td))
|
|
86
|
+
for _ in time_parts:
|
|
87
|
+
if secs is None:
|
|
88
|
+
secs = float(td.pop(-1))
|
|
89
|
+
elif mins is None:
|
|
90
|
+
mins = int(td.pop(-1))
|
|
91
|
+
elif hrs is None:
|
|
92
|
+
hrs = int(td.pop(-1))
|
|
93
|
+
else:
|
|
94
|
+
break
|
|
95
|
+
return datetime.timedelta(hours = hrs if hrs else 0,
|
|
96
|
+
minutes = mins if mins else 0,
|
|
97
|
+
seconds = secs if secs else 0)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def time_delta_str(secs, num_miliseconds = 2):
|
|
101
|
+
''' Timedelta string with specified number of miliseconds
|
|
102
|
+
'''
|
|
103
|
+
div = 10**num_miliseconds if num_miliseconds > 0 else 1
|
|
104
|
+
td_str = str(datetime.timedelta(seconds = math.ceil(secs*div)/div)).rstrip('0')
|
|
105
|
+
if td_str.endswith(':'):
|
|
106
|
+
td_str = '{}00'.format(td_str)
|
|
107
|
+
return td_str
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def percentile(values, percent):
|
|
111
|
+
'''
|
|
112
|
+
Find the percentile for a list of values
|
|
113
|
+
'''
|
|
114
|
+
if not values:
|
|
115
|
+
return None
|
|
116
|
+
else:
|
|
117
|
+
values.sort()
|
|
118
|
+
|
|
119
|
+
dv = (len(values) - 1) * percent / 100
|
|
120
|
+
floor = math.floor(dv)
|
|
121
|
+
|
|
122
|
+
ceil = math.ceil(dv)
|
|
123
|
+
if floor == ceil:
|
|
124
|
+
return values[int(dv)]
|
|
125
|
+
|
|
126
|
+
dc = values[int(floor)] * (ceil - dv)
|
|
127
|
+
dt = values[int(ceil)] * (dv - floor)
|
|
128
|
+
|
|
129
|
+
return dc + dt
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def median(l):
|
|
133
|
+
# the 50th percentile
|
|
134
|
+
return functools.partial(MiscHelpers.percentile, percent=50)(l)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ImageLoader:
|
|
138
|
+
@staticmethod
|
|
139
|
+
def load_image_from_url(url):
|
|
140
|
+
''' Loads image from an URL
|
|
141
|
+
'''
|
|
142
|
+
img = None
|
|
143
|
+
try:
|
|
144
|
+
responce = urllib.request.urlopen(url, timeout = 5)
|
|
145
|
+
except urllib.error.URLError as e:
|
|
146
|
+
print('A problem while retrieving image: "{}"'.format(e))
|
|
147
|
+
else:
|
|
148
|
+
content_type = dict(responce.getheaders())['Content-Type']
|
|
149
|
+
if content_type.split('/')[0] != 'image':
|
|
150
|
+
print('URL did not seem to return a valid image')
|
|
151
|
+
print('Received Content-Type: {}'.format(content_type))
|
|
152
|
+
else:
|
|
153
|
+
img = responce.read()
|
|
154
|
+
|
|
155
|
+
return img
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def load_image_from_file(fpath):
|
|
159
|
+
''' Loads an image from disk via file path
|
|
160
|
+
'''
|
|
161
|
+
img = None
|
|
162
|
+
if fpath:
|
|
163
|
+
with open(fpath, 'rb') as f:
|
|
164
|
+
img = f.read()
|
|
165
|
+
|
|
166
|
+
return img
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def load_image(path_or_url):
|
|
170
|
+
''' Loads an image from an URL or a file path
|
|
171
|
+
'''
|
|
172
|
+
url_parts = urlparse(path_or_url)
|
|
173
|
+
if url_parts.scheme in (None, '') and url_parts.netloc in (None, ''):
|
|
174
|
+
return ImageLoader.load_image_from_file(path_or_url)
|
|
175
|
+
|
|
176
|
+
if url_parts.scheme == 'file':
|
|
177
|
+
fpath = format(url_parts.path)
|
|
178
|
+
if url_parts.netloc == '~':
|
|
179
|
+
fpath = '~{}'.format(fpath)
|
|
180
|
+
return ImageLoader.load_image_from_file(path_or_url)
|
|
181
|
+
|
|
182
|
+
return ImageLoader.load_image_from_url(path_or_url)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# Quick dev test
|
|
186
|
+
if __name__ == '__main__':
|
|
187
|
+
td = MiscHelpers.time_delta('00:24.5764654645464')
|
|
188
|
+
print(td)
|
|
189
|
+
print(MiscHelpers.time_delta_str(td.total_seconds(), num_miliseconds = -1))
|
|
190
|
+
|
|
191
|
+
a = [2.08, 2.11, 2.18, 2.18, 2.2, 2.2, 2.21, 2.21, 2.3, 2.8, 2.97, 3.21, 3.4, 3.71, 3.8, 3.9, 4.21, 4.3, 4.4]
|
|
192
|
+
print(MiscHelpers.median(a))
|
|
193
|
+
|
|
194
|
+
print(MiscHelpers.percentile(a, 25))
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# coding=utf8
|
|
2
|
+
## Copyright (c) 2014 Arseniy Kuznetsov
|
|
3
|
+
##
|
|
4
|
+
## This program is free software; you can redistribute it and/or
|
|
5
|
+
## modify it under the terms of the GNU General Public License
|
|
6
|
+
## as published by the Free Software Foundation; either version 2
|
|
7
|
+
## of the License, or (at your option) any later version.
|
|
8
|
+
##
|
|
9
|
+
## This program is distributed in the hope that it will be useful,
|
|
10
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
## GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from enum import IntEnum
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FFmpegCommands:
|
|
19
|
+
''' Common FFmpeg commands / options
|
|
20
|
+
'''
|
|
21
|
+
MAP_ALL_STREAMS = ' -map 0'
|
|
22
|
+
COPY_CODECS = ' -c copy'
|
|
23
|
+
|
|
24
|
+
# queue size
|
|
25
|
+
MUXING_QUEUE_SIZE = ' -max_muxing_queue_size 1024'
|
|
26
|
+
|
|
27
|
+
# excluding streams
|
|
28
|
+
DISABLE_VIDEO = ' -vn'
|
|
29
|
+
DISABLE_AUDIO = ' -an'
|
|
30
|
+
DISABLE_SUBTITLES = ' -sn'
|
|
31
|
+
|
|
32
|
+
# Conversion options
|
|
33
|
+
CONVERT_COPY_VBR_QUALITY = ' -q:v 0 -q:a 0'
|
|
34
|
+
CONVERT_LOSSLESS = ' CONVERT_LOSSLESS_IF_POSSIBLE'
|
|
35
|
+
CONVERT_LOSSLESS_ALAC = ' -q:v 0 -acodec alac'
|
|
36
|
+
CONVERT_LOSSLESS_FLAC = ' -q:v 0 -acodec flac'
|
|
37
|
+
CONVERT_CHANGE_CONTAINER = ' -c copy -copyts'
|
|
38
|
+
|
|
39
|
+
# Log level
|
|
40
|
+
LOG_LEVEL_ERROR = ' -v error'
|
|
41
|
+
LOG_LEVEL_QUIET = ' -v quiet'
|
|
42
|
+
|
|
43
|
+
# Segment
|
|
44
|
+
SEGMENT = ' -f segment'
|
|
45
|
+
SEGMENT_TIME = ' -segment_time'
|
|
46
|
+
SEGMENT_TIMES = ' -segment_times'
|
|
47
|
+
SEGMENT_RESET_TIMESTAMPS = ' -reset_timestamps 1'
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def exclude_input_stream(stream_idx):
|
|
51
|
+
return ' -map -0:{}'.format(stream_idx)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def include_input_stream(stream_idx):
|
|
55
|
+
return ' -map 0:{}'.format(stream_idx)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FFmpegBitMaskOptions(IntEnum):
|
|
59
|
+
''' FFmpeg commands / options bitmasks
|
|
60
|
+
'''
|
|
61
|
+
MAP_ALL_STREAMS = (1<<0)
|
|
62
|
+
COPY_CODECS = (1<<1)
|
|
63
|
+
|
|
64
|
+
MUXING_QUEUE_SIZE = (1<<4)
|
|
65
|
+
|
|
66
|
+
DISABLE_VIDEO = (1<<5)
|
|
67
|
+
DISABLE_AUDIO = (1<<6)
|
|
68
|
+
DISABLE_SUBTITLES = (1<<7)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def ff_general_options(cls, ff_gbm_options):
|
|
72
|
+
options_str = ''
|
|
73
|
+
if ff_gbm_options:
|
|
74
|
+
for bm_option in cls:
|
|
75
|
+
if bm_option & ff_gbm_options == bm_option:
|
|
76
|
+
options_str = ''.join((options_str, cls._option_str_value(bm_option)))
|
|
77
|
+
return options_str
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _option_str_value(bm_option):
|
|
81
|
+
if bm_option == FFmpegBitMaskOptions.MAP_ALL_STREAMS:
|
|
82
|
+
return FFmpegCommands.MAP_ALL_STREAMS
|
|
83
|
+
elif bm_option == FFmpegBitMaskOptions.COPY_CODECS:
|
|
84
|
+
return FFmpegCommands.COPY_CODECS
|
|
85
|
+
|
|
86
|
+
elif bm_option == FFmpegBitMaskOptions.DISABLE_VIDEO:
|
|
87
|
+
return FFmpegCommands.DISABLE_VIDEO
|
|
88
|
+
elif bm_option == FFmpegBitMaskOptions.DISABLE_AUDIO:
|
|
89
|
+
return FFmpegCommands.DISABLE_AUDIO
|
|
90
|
+
elif bm_option == FFmpegBitMaskOptions.DISABLE_SUBTITLES:
|
|
91
|
+
return FFmpegCommands.DISABLE_SUBTITLES
|
|
92
|
+
elif bm_option == FFmpegBitMaskOptions.MUXING_QUEUE_SIZE:
|
|
93
|
+
return FFmpegCommands.MUXING_QUEUE_SIZE
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
return ''
|
|
97
|
+
|
|
98
|
+
# Quick dev test
|
|
99
|
+
if __name__ == '__main__':
|
|
100
|
+
copy_codecs = True
|
|
101
|
+
options = 0
|
|
102
|
+
options |= FFmpegBitMaskOptions.DISABLE_SUBTITLES
|
|
103
|
+
if copy_codecs:
|
|
104
|
+
options |= FFmpegBitMaskOptions.COPY_CODECS
|
|
105
|
+
options |= FFmpegBitMaskOptions.DISABLE_VIDEO
|
|
106
|
+
options |= FFmpegBitMaskOptions.MUXING_QUEUE_SIZE
|
|
107
|
+
print(FFmpegBitMaskOptions.ff_general_options(options))
|
|
108
|
+
|
|
109
|
+
exclude_stream = 1
|
|
110
|
+
print('exclude video stream {}: {}'.format(exclude_stream, FFmpegCommands.exclude_input_stream(exclude_stream)))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|