1
|
1 |
|
import re |
2
|
1 |
|
import time |
3
|
|
|
|
4
|
1 |
|
import attr |
5
|
1 |
|
from music_album_creation.tracks_parsing import StringParser |
6
|
|
|
|
7
|
|
|
|
8
|
|
|
##################################### |
9
|
1 |
|
def segmentation_information(tracks_timestamps_info): |
10
|
|
|
""" |
11
|
|
|
Converts input Nx2 (timestamps) list of lists to Nx3 (timestamps) list of lists. The exception being the last list that has 2 elements\n |
12
|
|
|
The input list's inner lists' elements are 'track_name' and 'starting_timestamp' in hhmmss format.\n |
13
|
|
|
:param list of lists tracks_timestamps_info: each inner list should contain track title (no need for number and without extension) |
14
|
|
|
and starting time stamp in hh:mm:ss format |
15
|
|
|
:return: each iner list contains track path and timestamp in seconds |
16
|
|
|
:rtype: list of lists |
17
|
|
|
""" |
18
|
1 |
|
return [list(_) for _ in _generate_segmentation_spans(tracks_timestamps_info)] |
19
|
|
|
|
20
|
|
|
|
21
|
1 |
|
def _generate_segmentation_spans(tracks_info): |
22
|
|
|
""" |
23
|
|
|
Given a data list, with each element representing an album's track (as an inner 2-element list with 1st element the 'track_name' and 2nd a timetamp in hh:mm:ss format (the track starts it's playback at that timestamp in relation with the total album playtime), the path to the alum file at hand and the desired output directory or potentially storing the track files, |
24
|
|
|
generates 3-length tuples with the track_file_path, starting timestamp and ending timestamp. Purpose is for the yielded tripplets to be digested for audio segmentation. The exception being the last tuple yielded that has 2 elements; it naturally misses the ending timestamp.\n |
25
|
|
|
:param list tracks_info: |
26
|
|
|
:returns: 3-element tuples with track_file_path, starting_timestamp, ending_timestamp |
27
|
|
|
:rtype: tuple |
28
|
|
|
""" |
29
|
1 |
|
track_index_generator = iter((lambda x: str(x) if 9 < x else '0' + str(x))(_) for _ in range(1, len(tracks_info) + 1)) |
30
|
1 |
|
for i in range(len(tracks_info)-1): |
31
|
1 |
|
if Timestamp(tracks_info[i + 1][1]) <= Timestamp(tracks_info[i][1]): |
32
|
1 |
|
raise TrackTimestampsSequenceError( |
33
|
|
|
"Track '{} - {}' starting timestamp '{}' should be 'bigger' than track's '{} - {}'; '{}'".format( |
34
|
|
|
i + 2, tracks_info[i + 1][0], tracks_info[i + 1][1], |
35
|
|
|
i + 1, tracks_info[i][0], tracks_info[i][1])) |
36
|
1 |
|
yield ( |
37
|
|
|
'{} - {}'.format(next(track_index_generator), tracks_info[i][0]), |
38
|
|
|
str(int(Timestamp(tracks_info[i][1]))), |
39
|
|
|
str(int(Timestamp(tracks_info[i + 1][1]))) |
40
|
|
|
) |
41
|
1 |
|
yield ( |
42
|
|
|
'{} - {}'.format(next(track_index_generator), tracks_info[-1][0]), |
43
|
|
|
str(int(Timestamp(tracks_info[-1][1]))), |
44
|
|
|
) |
45
|
|
|
|
46
|
1 |
|
def to_timestamps_info(tracks_durations_info): |
47
|
|
|
"""Call this method to transform a list of 2-legnth lists of track_name - duration_hhmmss pairs to the equivalent list of lists but with starting timestamps in hhmmss format inplace of the durations.\n |
48
|
|
|
:param list tracks_durations_info: eg: [['Know your enemy', '3:45'], ['Wake up', '4:53'], ['Testify', '4:32']] |
49
|
|
|
:return: eg: [['Know your enemy', '0:00'], ['Wake up', '3:45'], ['Testify', '8:38']] |
50
|
|
|
:rtype: list |
51
|
|
|
""" |
52
|
1 |
|
return [list(_) for _ in _gen_timestamp_data(tracks_durations_info)] |
53
|
|
|
|
54
|
|
|
|
55
|
1 |
|
def _gen_timestamp_data(tracks_duration_info): |
56
|
|
|
""" |
57
|
|
|
:param list of lists tracks_duration_info: each inner list has as 1st element a track name and as 2nd the track duration in hh:mm:s format |
58
|
|
|
:return: list of lists with timestamps instead of durations ready to feed for segmentation |
59
|
|
|
:rtype: list |
60
|
|
|
""" |
61
|
1 |
|
i = 1 |
62
|
1 |
|
p = Timestamp('0:00') |
63
|
1 |
|
yield tracks_duration_info[0][0], str(p) |
64
|
1 |
|
while i < len(tracks_duration_info): |
65
|
1 |
|
try: |
66
|
1 |
|
yield tracks_duration_info[i][0], str(p + Timestamp(tracks_duration_info[i - 1][1])) |
67
|
|
|
except WrongTimestampFormat as e: |
68
|
|
|
raise e |
69
|
1 |
|
p += Timestamp(tracks_duration_info[i - 1][1]) |
70
|
1 |
|
i += 1 |
71
|
|
|
|
72
|
|
|
############################################## |
73
|
1 |
|
@attr.s |
74
|
1 |
|
class SegmentationInformation(object): |
75
|
|
|
"""Encapsulates per track: ['track-name', 'start-timestamp', 'end-timestamp']. Last entry does not have 'end-timestamp' because the end of the album is implied.""" |
76
|
1 |
|
tracks_info = attr.ib(init=True) |
77
|
|
|
|
78
|
1 |
|
@classmethod |
79
|
|
|
def from_tracks_information(cls, tracks_information, hhmmss_type): |
80
|
1 |
|
if hhmmss_type.lower().startswith('timestamp'): |
81
|
1 |
|
return SegmentationInformation(segmentation_information(list(tracks_information))) |
82
|
|
|
return SegmentationInformation(segmentation_information(to_timestamps_info(list(tracks_information)))) |
83
|
|
|
|
84
|
1 |
|
@classmethod |
85
|
|
|
def from_multiline(cls, string, hhmmss_type): |
86
|
1 |
|
return cls.from_tracks_information(TracksInformation.from_multiline(string), hhmmss_type) |
87
|
|
|
|
88
|
1 |
|
def __len__(self): |
89
|
1 |
|
return len(self.tracks_info) |
90
|
|
|
|
91
|
1 |
|
def __getitem__(self, item): |
92
|
1 |
|
return self.tracks_info[item] |
93
|
|
|
|
94
|
1 |
|
@attr.s |
95
|
1 |
|
class TracksInformation(object): |
96
|
|
|
"""Encapsulates per track: ['track-name', 'hhmmss']""" |
97
|
1 |
|
tracks_data = attr.ib(init=True) |
98
|
1 |
|
track_names = attr.ib(init=False, default=attr.Factory(lambda self: [x[0] for x in self.tracks_data], takes_self=True)) |
99
|
1 |
|
hhmmss_list = attr.ib(init=False, default=attr.Factory(lambda self: [x[1] for x in self.tracks_data], takes_self=True)) |
100
|
|
|
|
101
|
1 |
|
@classmethod |
102
|
|
|
def from_multiline(cls, string): |
103
|
1 |
|
return TracksInformation(StringParser.parse_hhmmss_string(string)) |
104
|
|
|
|
105
|
1 |
|
@classmethod |
106
|
|
|
def from_multiline_interactive(cls, interactive_dialog): |
107
|
|
|
return TracksInformation(StringParser.parse_hhmmss_string(interactive_dialog())) |
108
|
|
|
|
109
|
1 |
|
def __len__(self): |
110
|
1 |
|
return len(self.tracks_data) |
111
|
|
|
|
112
|
1 |
|
def __getitem__(self, item): |
113
|
1 |
|
return self.tracks_data[item] |
114
|
|
|
|
115
|
|
|
########################################################## |
116
|
1 |
|
class Timestamp(object): |
117
|
1 |
|
instances = {} |
118
|
|
|
|
119
|
1 |
|
@classmethod |
120
|
|
|
def __str(cls, element): |
121
|
1 |
|
if len(element) == 1: |
122
|
1 |
|
return '0{}'.format(int(element)) |
123
|
1 |
|
return element |
124
|
|
|
|
125
|
1 |
|
@classmethod |
126
|
|
|
def __pos(cls, array): |
127
|
1 |
|
i = 0 |
128
|
1 |
|
while i < len(array) and array[i] == 0: |
129
|
|
|
i += 1 |
130
|
1 |
|
return i |
131
|
|
|
|
132
|
1 |
|
def __new__(cls, *args, **kwargs): |
133
|
1 |
|
hhmmss = args[0] |
134
|
1 |
|
m = re.compile(r'^(?:(\d?\d):){0,2}(\d?\d)$').search(hhmmss) |
135
|
1 |
|
if not m: |
136
|
1 |
|
raise WrongTimestampFormat("Timestamp given: '{}'. Please use the 'hh:mm:ss' format.".format(hhmmss)) |
137
|
1 |
|
groups = hhmmss.split(':') |
138
|
1 |
|
if not all([0 <= int(_) <= 60 for _ in groups]): |
139
|
1 |
|
raise WrongTimestampFormat("Timestamp given: '{}'. Please use the 'hh:mm:ss' format.".format(hhmmss)) |
140
|
|
|
|
141
|
1 |
|
ind = cls.__pos(groups) |
142
|
1 |
|
if len(groups) == 1: |
143
|
1 |
|
minlength_string = '{}:{}'.format(0, cls.__str(groups[0])) |
144
|
1 |
|
elif len(groups) - ind - 1 < 2: |
145
|
1 |
|
minlength_string = '{}:{}'.format(int(groups[-2]), cls.__str(groups[-1])) |
146
|
|
|
else: |
147
|
1 |
|
minlength_string = ':'.join([str(int(groups[ind]))] + [y for y in groups[ind + 1:]]) |
148
|
1 |
|
stripped_string = ':'.join((str(int(_)) for _ in minlength_string.split(':'))) |
|
|
|
|
149
|
|
|
|
150
|
1 |
|
if stripped_string in cls.instances: |
151
|
1 |
|
return cls.instances[stripped_string] |
152
|
1 |
|
x = super(Timestamp, cls).__new__(cls) |
153
|
1 |
|
x.__minlength_string = minlength_string |
154
|
1 |
|
x._a = 'gg' |
155
|
1 |
|
x.__stripped_string = stripped_string |
156
|
1 |
|
x.__s = sum([60 ** i * int(gr) for i, gr in enumerate(reversed(groups))]) |
157
|
1 |
|
cls.instances[x.__stripped_string] = x |
158
|
1 |
|
return x |
159
|
|
|
|
160
|
1 |
|
def __init__(self, hhmmss): |
161
|
|
|
pass |
162
|
|
|
|
163
|
1 |
|
@staticmethod |
164
|
|
|
def from_duration(seconds): |
165
|
1 |
|
return Timestamp(time.strftime('%H:%M:%S', time.gmtime(seconds))) |
166
|
|
|
|
167
|
1 |
|
def __int__(self): |
168
|
1 |
|
return self.__s |
169
|
|
|
|
170
|
1 |
|
def __repr__(self): |
171
|
|
|
return self.__minlength_string |
172
|
|
|
|
173
|
1 |
|
def __str__(self): |
174
|
1 |
|
return self.__minlength_string |
175
|
|
|
|
176
|
1 |
|
def __hash__(self): |
177
|
1 |
|
return self.__s |
178
|
|
|
|
179
|
1 |
|
def __eq__(self, other): |
180
|
1 |
|
return hash(self) == hash(other) |
181
|
|
|
|
182
|
1 |
|
def __lt__(self, other): |
183
|
1 |
|
return int(self) < int(other) |
184
|
|
|
|
185
|
1 |
|
def __le__(self, other): |
186
|
1 |
|
return int(self) <= int(other) |
187
|
|
|
|
188
|
1 |
|
def __gt__(self, other): |
189
|
1 |
|
return int(other) < int(self) |
190
|
|
|
|
191
|
1 |
|
def __ge__(self, other): |
192
|
1 |
|
return int(other) <= int(self) |
193
|
|
|
|
194
|
1 |
|
def __add__(self, other): |
195
|
1 |
|
return Timestamp.from_duration(int(self) + int(other)) |
196
|
|
|
|
197
|
1 |
|
def __sub__(self, other): |
198
|
1 |
|
return Timestamp.from_duration(int(self) - int(other)) |
199
|
|
|
|
200
|
|
|
|
201
|
|
|
class WrongTimestampFormat(Exception): pass |
202
|
|
|
class TrackTimestampsSequenceError(Exception): pass |
203
|
|
|
|