Skip to content

Commit 13f76d7

Browse files
committed
Premilinary commit
0 parents  commit 13f76d7

File tree

10 files changed

+3552
-0
lines changed

10 files changed

+3552
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mpv_thumbnail_script_server.lua
2+
mpv_thumbnail_script_client_osc.lua

Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
all: mpv_thumbnail_script
2+
3+
mpv_thumbnail_script: mpv_thumbnail_script_server mpv_thumbnail_script_client
4+
5+
mpv_thumbnail_script_server:
6+
./concat_files.py -r cat_server.json
7+
8+
mpv_thumbnail_script_client:
9+
./concat_files.py -r cat_osc.json
10+
11+
clean:
12+
rm mpv_thumbnail_script_server.lua mpv_thumbnail_script_client_osc.lua

cat_osc.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"output" : "mpv_thumbnail_script_client_osc.lua",
3+
"files" : [
4+
"lib/helpers.lua",
5+
"src/options.lua",
6+
"src/thumbnailer_shared.lua",
7+
"src/patched_osc.lua"
8+
],
9+
"header_prefix" : "--[ ",
10+
"header_suffix" : " ]--"
11+
}

cat_server.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"output" : "mpv_thumbnail_script_server.lua",
3+
"files" : [
4+
"lib/helpers.lua",
5+
"src/options.lua",
6+
"src/thumbnailer_shared.lua",
7+
"src/thumbnailer_server.lua"
8+
],
9+
"header_prefix" : "--[ ",
10+
"header_suffix" : " ]--"
11+
}

concat_files.py

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
#!/usr/bin/env python3
2+
import re
3+
import os
4+
import time
5+
import json
6+
import binascii
7+
8+
import argparse
9+
import hashlib
10+
import subprocess
11+
import datetime
12+
13+
14+
parser = argparse.ArgumentParser(description="Concatenate files to a target file, optionally writing target-changes back to source files")
15+
16+
parser.add_argument('config_file', metavar='CONFIG_FILE', help='Configuration file for concat')
17+
parser.add_argument('-o', '--output', metavar='OUTPUT', help='Override output filename')
18+
parser.add_argument('-w', '--watch', action='store_true', help='Watch files for any changes and map them between the target and source files')
19+
parser.add_argument('-r', '--release', action='store_true', help='Build a version without the section dividers')
20+
21+
22+
class FileWatcher(object):
23+
def __init__(self, file_list=[]):
24+
self.file_list = file_list
25+
self._mtimes = self._get_mtimes()
26+
27+
def _get_mtimes(self):
28+
return { filename : os.path.getmtime(filename) for filename in self.file_list if os.path.exists(filename) }
29+
30+
def get_changes(self):
31+
mtimes = self._get_mtimes()
32+
changes = [ filename for filename in self.file_list if self._mtimes.get(filename, 0) < mtimes.get(filename, 0) ]
33+
self._mtimes.update(mtimes)
34+
return changes
35+
36+
37+
class FileSection(object):
38+
def __init__(self, filename, content, modified):
39+
self.filename = filename
40+
self.content = content
41+
self.modified = modified
42+
self.hash = None
43+
44+
self.old_hash = None
45+
46+
if self.content:
47+
self.recalculate_hash()
48+
49+
def __repr__(self):
50+
hash_part = binascii.hexlify(self.hash).decode()[:7]
51+
if self.old_hash:
52+
hash_part += ' (' + binascii.hexlify(self.old_hash).decode()[:7] + ')'
53+
54+
return '<{} \'{}\' {}b {}>'.format(self.__class__.__name__, self.filename, len(self.content), hash_part)
55+
56+
def recalculate_hash(self):
57+
self.hash = hashlib.sha256(self.content.encode('utf-8')).digest()
58+
return self.hash
59+
60+
@classmethod
61+
def from_file(cls, filename):
62+
modified_time = os.path.getmtime(filename)
63+
with open(filename, 'r', encoding='utf-8') as in_file:
64+
content = in_file.read()
65+
66+
return cls(filename=filename,
67+
content=content,
68+
modified=modified_time)
69+
# hash=hash)
70+
71+
72+
class Concatter(object):
73+
SECTION_REGEX_BASE = r'FileConcat-([SE]) (.+?) HASH:(.+?)'
74+
SECTION_HEADER_FORMAT_BASE = 'FileConcat-{kind} {filename} HASH:{hash}'
75+
def __init__(self, config, working_directory=''):
76+
self.output_filename = config.get('output', 'output.txt')
77+
self.file_list = config.get('files', [])
78+
self.section_header_prefix = config.get('header_prefix', '')
79+
self.section_header_suffix = config.get('header_suffix', '')
80+
self.working_directory = working_directory
81+
82+
self.version_metafile = None
83+
84+
self.newline = '\n'
85+
86+
self.section_header_format = self.section_header_prefix + self.SECTION_HEADER_FORMAT_BASE + self.section_header_suffix
87+
self.section_header_regex = re.compile(r'^' + re.escape(self.section_header_prefix) + self.SECTION_REGEX_BASE + re.escape(self.section_header_suffix) + r'$')
88+
89+
90+
def split_output_file(self):
91+
file_sections = []
92+
if not os.path.exists(self.output_filename):
93+
return file_sections
94+
95+
modified_time = os.path.getmtime(self.output_filename)
96+
with open(self.output_filename, 'r', encoding='utf-8') as in_file:
97+
current_section = None
98+
section_lines = []
99+
100+
for line in in_file:
101+
header_match = self.section_header_regex.match(line)
102+
if header_match:
103+
is_start = header_match.group(1) == 'S'
104+
section_filename = header_match.group(2)
105+
section_hash = binascii.unhexlify(header_match.group(3))
106+
107+
if is_start and current_section is None:
108+
current_section = FileSection(section_filename, None, modified_time)
109+
elif not is_start and current_section:
110+
current_section.content = ''.join(section_lines)
111+
current_section.recalculate_hash()
112+
current_section.old_hash = section_hash
113+
114+
file_sections.append(current_section)
115+
116+
current_section = None
117+
section_lines = []
118+
else:
119+
section_lines.append(line)
120+
121+
if current_section is not None:
122+
raise Exception('Missing file end marker! For ' + current_section.filename)
123+
124+
return file_sections
125+
126+
127+
def read_source_files(self):
128+
file_sections = []
129+
130+
for filename in self.file_list:
131+
# The version metafile
132+
if filename == '<version>':
133+
file_section = self.version_metafile
134+
else:
135+
file_path = os.path.join(self.working_directory, filename)
136+
if not os.path.exists(file_path):
137+
raise Exception("File '{}' is missing!".format(file_path))
138+
139+
file_section = FileSection.from_file(file_path)
140+
file_section.filename = filename
141+
142+
# Figure out newline to be used
143+
# if self.newline is None:
144+
# self.newline = '\r\n' if '\r\n' in file_section.content else '\n'
145+
146+
if not file_section.content.endswith(self.newline):
147+
file_section.content += self.newline
148+
file_section.recalculate_hash()
149+
150+
file_sections.append(file_section)
151+
return file_sections
152+
153+
154+
def concatenate_file_sections(self, file_sections, insert_section_headers=True):
155+
with open(self.output_filename, 'w', newline='\n', encoding='utf-8') as out_file:
156+
for file_section in file_sections:
157+
if insert_section_headers:
158+
section_hash = binascii.hexlify(file_section.recalculate_hash()).decode()
159+
160+
out_file.write(self.section_header_format.format(kind='S', filename=file_section.filename, hash=section_hash) + self.newline)
161+
out_file.write(file_section.content)
162+
out_file.write(self.section_header_format.format(kind='E', filename=file_section.filename, hash=section_hash) + self.newline)
163+
else:
164+
out_file.write(file_section.content)
165+
166+
167+
def write_file_sections_back(self, file_sections):
168+
for file_section in file_sections:
169+
# Skip version metafile
170+
if file_section is self.version_metafile: continue
171+
172+
file_path = os.path.join(self.working_directory, file_section.filename)
173+
174+
# Backup target file if it exists
175+
if os.path.exists(file_path):
176+
bak_filename = file_path + '.bak'
177+
if os.path.exists(bak_filename):
178+
os.remove(bak_filename)
179+
os.rename(file_path, bak_filename)
180+
181+
# Write contents
182+
with open(file_path, 'w', newline='\n', encoding='utf-8') as out_file:
183+
out_file.write(file_section.content)
184+
185+
186+
def _map_sections(self, source_sections, target_sections):
187+
target_map = {s.filename: s for s in target_sections}
188+
189+
source_to_target = []
190+
target_to_source = []
191+
192+
for source_section in source_sections:
193+
if source_section is self.version_metafile: continue
194+
195+
target_section = target_map.get(source_section.filename)
196+
197+
if not target_section:
198+
# Target doesn't have this section at all (or is completely empty)
199+
source_to_target.append(source_section) # Write section to target file
200+
else:
201+
source_section.old_hash = target_section.old_hash # Used to check changes on rewrite
202+
source_newer = source_section.modified > target_section.modified
203+
204+
# Target and source differ
205+
if source_section.hash != target_section.hash:
206+
if source_newer:
207+
# If source file is newer than target, use it
208+
source_to_target.append(source_section)
209+
else:
210+
# Use target section to rewrite target section AND source file
211+
target_section.old_hash = target_section.hash # Hack to skip target rewrite
212+
source_to_target.append(target_section)
213+
target_to_source.append(target_section)
214+
else:
215+
# No change in files so just use the source file
216+
source_to_target.append(source_section)
217+
218+
return source_to_target, target_to_source
219+
220+
221+
def process_changes_in_files(self):
222+
source_sections = self.read_source_files()
223+
target_sections = self.split_output_file()
224+
225+
source_to_target, target_to_source = self._map_sections(source_sections, target_sections)
226+
227+
changed_sections = [s for s in source_to_target if s.hash != s.old_hash]
228+
if changed_sections:
229+
self.concatenate_file_sections(source_to_target)
230+
231+
if target_to_source:
232+
self.write_file_sections_back(target_to_source)
233+
234+
return changed_sections, target_to_source
235+
236+
def plain_concat(self):
237+
source_sections = self.read_source_files()
238+
self.concatenate_file_sections(source_sections, False)
239+
240+
241+
def _create_version_metafile(config, config_dirname):
242+
repo_dir = os.path.join(config_dirname, config.get('repo_dir', ''))
243+
try:
244+
git_branch = subprocess.check_output(['git', '-C', repo_dir, 'symbolic-ref', '--short', '-q', 'HEAD'], stderr=subprocess.DEVNULL).decode().strip()
245+
git_commit = subprocess.check_output(['git', '-C', repo_dir, 'rev-parse', '--short', '-q', 'HEAD'], stderr=subprocess.DEVNULL).decode().strip()
246+
except:
247+
git_branch = None
248+
git_commit = None
249+
250+
if not git_branch:
251+
git_branch = 'unknown'
252+
253+
if git_commit:
254+
git_commit_short = git_commit[:7]
255+
else:
256+
git_commit = git_commit_short = 'unknown'
257+
258+
project_version_file = config.get('version_file')
259+
if project_version_file:
260+
with open(project_version_file, 'r') as in_file:
261+
project_version = in_file.read().strip()
262+
else:
263+
project_version = 'unknown'
264+
265+
template_data = {
266+
'version' : project_version,
267+
268+
'branch' : git_branch,
269+
'commit' : git_commit,
270+
'commit_short' : git_commit_short,
271+
272+
'now' : datetime.datetime.now(),
273+
'utc_now' : datetime.datetime.utcnow(),
274+
}
275+
276+
version_template_file = config.get('version_template_file')
277+
if version_template_file:
278+
with open(os.path.join(config_dirname, version_template_file), 'r') as in_file:
279+
version_template = in_file.read()
280+
else:
281+
version_template = ''
282+
283+
version_metafile = FileSection('<version>', version_template.format(**template_data), 0)
284+
return version_metafile
285+
286+
287+
def _print_change_writes(source_to_target, target_to_source):
288+
if source_to_target:
289+
print('SOURCE -> TARGET')
290+
print(source_to_target)
291+
if target_to_source:
292+
print('TARGET -> SOURCE')
293+
print(target_to_source)
294+
if not source_to_target and not target_to_source:
295+
print('No changes.')
296+
297+
298+
if __name__ == '__main__':
299+
args = parser.parse_args()
300+
301+
if not os.path.exists(args.config_file):
302+
print('Unable to find given configuration file \'{}\''.format(args.config_file))
303+
exit(1)
304+
305+
try:
306+
with open(args.config_file, 'r') as in_file:
307+
config = json.load(in_file)
308+
except:
309+
print('Unable to read given configuration file \'{}\''.format(args.config_file))
310+
exit(1)
311+
312+
config_dirname = os.path.dirname(args.config_file)
313+
if args.output:
314+
config['output'] = args.output
315+
else:
316+
# Make output be relative to config file
317+
config['output'] = os.path.join(config_dirname, config['output'])
318+
319+
concatter = Concatter(config, config_dirname)
320+
concatter.version_metafile = _create_version_metafile(config, config_dirname)
321+
322+
if not concatter.file_list:
323+
print('No files listed in configuration!')
324+
exit(1)
325+
326+
if not args.watch:
327+
if args.release:
328+
concatter.plain_concat()
329+
print("Concatenated source files to '{}'".format(concatter.output_filename))
330+
else:
331+
s2t, t2s = concatter.process_changes_in_files()
332+
_print_change_writes(s2t, t2s)
333+
else:
334+
tracked_files_list = list(concatter.file_list)
335+
tracked_files_list.append(concatter.output_filename)
336+
337+
file_watcher = FileWatcher(tracked_files_list)
338+
339+
print('Watching changes for', len(tracked_files_list), 'files...')
340+
while True:
341+
changes = file_watcher.get_changes()
342+
343+
if changes:
344+
print("------------------------", changes)
345+
if args.release:
346+
concatter.plain_concat()
347+
print("Concatenated source files to '{}'".format(concatter.output_filename))
348+
else:
349+
s2t, t2s = concatter.process_changes_in_files()
350+
_print_change_writes(s2t, t2s)
351+
# Grab new mtimes
352+
file_watcher.get_changes()
353+
354+
time.sleep(0.25)
355+

0 commit comments

Comments
 (0)