Skip to content

Commit 5e9dfd7

Browse files
TJSeitCopilot1337-server
authored
[BUGFIX] - Fix track filename mismatch issue automatic-ripping-machine#1530 (automatic-ripping-machine#1594)
* Fix off-by-one track filename bug when moving files after rip (bug automatic-ripping-machine#1530) * updating file name matching logic * reverting uneeded change * using authoritative file naming Authoritative Filename Propagation: The filename variable is defined as the authoritative source of truth for the destination path (filepathname). Before executing the HandBrake command, track.filename and track.orig_filename are updated with the authoritative filename. Database Commit: The updated values of track.filename are committed to the database before proceeding with the HandBrake command. Error Handling: If HandBrake fails, the authoritative name is already saved in the database, ensuring consistency for debugging and recovery. * Update handbrake.py * minimized changes * Remove filename assignment in handbrake.py Remove assignment of track.filename and track.orig_filename. * Initial plan * Fix filename path duplication bug in handbrake_mkv function Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> * Fix filename path duplication in handbrake_main_feature function Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> * Initial plan * Fix MakeMKV track numbering to be 1-indexed for consistency with HandBrake Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> * Fix logging message - remove incorrect -1 from no_of_titles Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> * Initial plan * Implement fuzzy file matching for MKV transcoding filename mismatches Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> * Update test file to be standalone and fix flake8 issues Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> * Initial plan * Fix PEP8 compliance issues in test_ripper_makemkv_track_numbering.py Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> * Initial plan * Clean up PR: remove track numbering changes, keep file matching fix Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> * Bump version from 2.20.4 to 2.20.5 * Initial plan * Refactor handbrake.py to reduce code duplication Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> * Further cleanup: remove redundant error logging in handbrake_mkv Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> * Initial plan * Fix SonarQube code duplication issues in test file Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> * Initial plan * Fix useless self-assignment in handbrake.py line 134 Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> * Resolving SonarQube issues and bumping version * Removing unnecessary try catch --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TJSeit <7307974+TJSeit@users.noreply.github.com> Co-authored-by: 1337-server <sndspamfilter@gmail.com>
1 parent 8e3af93 commit 5e9dfd7

3 files changed

Lines changed: 306 additions & 65 deletions

File tree

arm/ripper/handbrake.py

Lines changed: 82 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,72 @@
1515
PROCESS_COMPLETE = "Handbrake processing complete"
1616

1717

18+
def run_handbrake_command(cmd, track=None, track_number=None):
19+
"""
20+
Execute a HandBrake command and handle errors consistently.
21+
22+
:param cmd: The HandBrake command to execute
23+
:param track: Optional track object to update status
24+
:param track_number: Optional track number for error messages
25+
:return: Output from HandBrake command
26+
:raises subprocess.CalledProcessError: If HandBrake fails
27+
"""
28+
logging.debug(f"Sending command: {cmd}")
29+
30+
try:
31+
hand_brake_output = subprocess.check_output(
32+
cmd,
33+
shell=True
34+
).decode("utf-8")
35+
logging.debug(f"Handbrake exit code: {hand_brake_output}")
36+
if track:
37+
track.status = "success"
38+
return hand_brake_output
39+
except subprocess.CalledProcessError as hb_error:
40+
if track_number:
41+
err = f"Handbrake encoding of title {track_number} failed with code: {hb_error.returncode}" \
42+
f"({hb_error.output})"
43+
else:
44+
err = f"Call to handbrake failed with code: {hb_error.returncode}({hb_error.output})"
45+
logging.error(err)
46+
if track:
47+
track.status = "fail"
48+
track.error = err
49+
raise subprocess.CalledProcessError(hb_error.returncode, cmd)
50+
51+
52+
def build_handbrake_command(srcpath, filepathname, hb_preset, hb_args, logfile,
53+
track_number=None, main_feature=False):
54+
"""
55+
Build a HandBrake command string with consistent formatting.
56+
57+
:param srcpath: Path to source for HB (dvd or files)
58+
:param filepathname: Full output path including filename
59+
:param hb_preset: HandBrake preset to use
60+
:param hb_args: Additional HandBrake arguments
61+
:param logfile: Logfile for HB to redirect output to
62+
:param track_number: Optional track number to encode
63+
:param main_feature: Whether to use --main-feature flag
64+
:return: Formatted command string
65+
"""
66+
cmd = f"nice {cfg.arm_config['HANDBRAKE_CLI']} " \
67+
f"-i {shlex.quote(srcpath)} " \
68+
f"-o {shlex.quote(filepathname)} "
69+
70+
if main_feature:
71+
cmd += "--main-feature "
72+
73+
cmd += f"--preset \"{hb_preset}\" "
74+
75+
if track_number is not None:
76+
cmd += f"-t {track_number} "
77+
78+
cmd += f"{hb_args} " \
79+
f">> {logfile} 2>&1"
80+
81+
return cmd
82+
83+
1884
def handbrake_sleep_check(job):
1985
"""Wait until there is a spot to transcode.
2086
@@ -42,7 +108,7 @@ def handbrake_main_feature(srcpath, basepath, logfile, job):
42108
handbrake_sleep_check(job)
43109
logging.info("Starting DVD Movie main_feature processing")
44110

45-
filename = os.path.join(basepath, job.title + "." + cfg.arm_config["DEST_EXT"])
111+
filename = job.title + "." + cfg.arm_config["DEST_EXT"]
46112
filepathname = os.path.join(basepath, filename)
47113
logging.info(f"Ripping title main_feature to {shlex.quote(filepathname)}")
48114

@@ -58,28 +124,16 @@ def handbrake_main_feature(srcpath, basepath, logfile, job):
58124
db.session.commit()
59125

60126
hb_args, hb_preset = correct_hb_settings(job)
61-
cmd = f"nice {cfg.arm_config['HANDBRAKE_CLI']} " \
62-
f"-i {shlex.quote(srcpath)} " \
63-
f"-o {shlex.quote(filepathname)} " \
64-
f"--main-feature " \
65-
f"--preset \"{hb_preset}\" " \
66-
f"{hb_args} " \
67-
f">> {logfile} 2>&1"
68-
69-
logging.debug(f"Sending command: {cmd}")
127+
cmd = build_handbrake_command(srcpath, filepathname, hb_preset, hb_args, logfile, main_feature=True)
70128

71129
try:
72-
subprocess.check_output(cmd, shell=True).decode("utf-8")
130+
run_handbrake_command(cmd, track)
73131
logging.info("Handbrake call successful")
74-
track.status = "success"
75-
except subprocess.CalledProcessError as hb_error:
76-
err = f"Call to handbrake failed with code: {hb_error.returncode}({hb_error.output})"
77-
logging.error(err)
78-
track.status = "fail"
79-
track.error = job.errors = err
132+
except subprocess.CalledProcessError:
133+
job.errors = track.error
80134
job.status = JobState.FAILURE.value
81135
db.session.commit()
82-
raise subprocess.CalledProcessError(hb_error.returncode, cmd)
136+
raise
83137

84138
logging.info(PROCESS_COMPLETE)
85139
logging.debug(f"\n\r{job.pretty_table()}")
@@ -123,39 +177,21 @@ def handbrake_all(srcpath, basepath, logfile, job):
123177
logging.info(f"Processing track #{track.track_number} of {job.no_of_titles}. "
124178
f"Length is {track.length} seconds.")
125179

126-
filename = f"title_{track.track_number}.{cfg.arm_config['DEST_EXT']}"
127-
filepathname = os.path.join(basepath, filename)
180+
track.filename = track.orig_filename = f"title_{track.track_number}.{cfg.arm_config['DEST_EXT']}"
181+
filepathname = os.path.join(basepath, track.filename)
128182

129183
logging.info(f"Transcoding title {track.track_number} to {shlex.quote(filepathname)}")
130184

131-
track.filename = track.orig_filename = filename
132185
db.session.commit()
133186

134-
cmd = f"nice {cfg.arm_config['HANDBRAKE_CLI']} " \
135-
f"-i {shlex.quote(srcpath)} " \
136-
f"-o {shlex.quote(filepathname)} " \
137-
f"--preset \"{hb_preset}\" " \
138-
f"-t {track.track_number} " \
139-
f"{hb_args} " \
140-
f">> {logfile} 2>&1"
141-
142-
logging.debug(f"Sending command: {cmd}")
187+
cmd = build_handbrake_command(srcpath, filepathname, hb_preset, hb_args, logfile,
188+
track_number=track.track_number)
143189

144190
try:
145-
hand_brake_output = subprocess.check_output(
146-
cmd,
147-
shell=True
148-
).decode("utf-8")
149-
logging.debug(f"Handbrake exit code: {hand_brake_output}")
150-
track.status = "success"
151-
except subprocess.CalledProcessError as hb_error:
152-
err = f"Handbrake encoding of title {track.track_number} failed with code: {hb_error.returncode}" \
153-
f"({hb_error.output})"
154-
logging.error(err)
155-
track.status = "fail"
156-
track.error = err
191+
run_handbrake_command(cmd, track, track.track_number)
192+
except subprocess.CalledProcessError:
157193
db.session.commit()
158-
raise subprocess.CalledProcessError(hb_error.returncode, cmd)
194+
raise
159195

160196
track.ripped = True
161197
db.session.commit()
@@ -208,30 +244,13 @@ def handbrake_mkv(srcpath, basepath, logfile, job):
208244
track.filename = destfile + "." + cfg.arm_config["DEST_EXT"]
209245
logging.debug("UPDATED filename: " + track.filename)
210246
db.session.commit()
211-
filename = os.path.join(basepath, destfile + "." + cfg.arm_config["DEST_EXT"])
247+
filename = destfile + "." + cfg.arm_config["DEST_EXT"]
212248
filepathname = os.path.join(basepath, filename)
213249

214250
logging.info(f"Transcoding file {shlex.quote(files)} to {shlex.quote(filepathname)}")
215251

216-
cmd = f'nice {cfg.arm_config["HANDBRAKE_CLI"]} ' \
217-
f'-i {shlex.quote(srcpathname)} ' \
218-
f'-o {shlex.quote(filepathname)} ' \
219-
f'--preset "{hb_preset}" {hb_args} >> {logfile} 2>&1'
220-
221-
logging.debug(f"Sending command: {cmd}")
222-
223-
try:
224-
hand_break_output = subprocess.check_output(
225-
cmd,
226-
shell=True
227-
).decode("utf-8")
228-
logging.debug(f"Handbrake exit code: {hand_break_output}")
229-
except subprocess.CalledProcessError as hb_error:
230-
err = f"Handbrake encoding of file {shlex.quote(files)} failed with code: {hb_error.returncode}" \
231-
f"({hb_error.output})"
232-
logging.error(err)
233-
raise subprocess.CalledProcessError(hb_error.returncode, cmd)
234-
# job.errors.append(f)
252+
cmd = build_handbrake_command(srcpathname, filepathname, hb_preset, hb_args, logfile)
253+
run_handbrake_command(cmd)
235254

236255
logging.info(PROCESS_COMPLETE)
237256
logging.debug(f"\n\r{job.pretty_table()}")

arm/ripper/utils.py

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,95 @@ def move_files(base_path, filename, job, is_main_feature=False):
226226
return movie_path
227227

228228

229+
def _calculate_filename_similarity(expected_base, actual_base):
230+
"""
231+
Calculate similarity score between two filenames.
232+
233+
:param str expected_base: Expected filename without extension
234+
:param str actual_base: Actual filename without extension
235+
:return int: Similarity score
236+
"""
237+
score = 0
238+
min_len = min(len(expected_base), len(actual_base))
239+
240+
# Count matching characters from the start
241+
for i in range(min_len):
242+
if expected_base[i] == actual_base[i]:
243+
score += 1
244+
else:
245+
break
246+
247+
# Count matching characters from the end
248+
for i in range(1, min_len + 1):
249+
if expected_base[-i] == actual_base[-i]:
250+
score += 1
251+
else:
252+
break
253+
254+
# Bonus for similar length
255+
length_diff = abs(len(expected_base) - len(actual_base))
256+
if length_diff <= 2: # Within 2 characters difference
257+
score += (3 - length_diff) * 2
258+
259+
return score
260+
261+
262+
def find_matching_file(expected_file):
263+
"""
264+
Find a file that matches the expected filename, handling minor naming discrepancies.
265+
This is particularly useful for MKV files transcoded by HandBrake where the output
266+
filename may differ slightly from what's stored in the database.
267+
268+
:param str expected_file: The full path to the expected file
269+
:return str: The actual file path if found, or the original expected_file if no match
270+
"""
271+
if os.path.isfile(expected_file):
272+
return expected_file
273+
274+
directory = os.path.dirname(expected_file)
275+
expected_filename = os.path.basename(expected_file)
276+
277+
if not os.path.isdir(directory):
278+
return expected_file
279+
280+
expected_base, expected_ext = os.path.splitext(expected_filename)
281+
282+
# Get candidate files with same extension
283+
try:
284+
files_in_dir = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
285+
except OSError:
286+
return expected_file
287+
288+
candidate_files = []
289+
for file in files_in_dir:
290+
base, ext = os.path.splitext(file)
291+
if ext.lower() == expected_ext.lower():
292+
candidate_files.append((file, base))
293+
294+
if not candidate_files:
295+
return expected_file
296+
297+
# Find best match
298+
best_match = None
299+
best_score = 0
300+
301+
for file, base in candidate_files:
302+
score = _calculate_filename_similarity(expected_base, base)
303+
if score > best_score:
304+
best_score = score
305+
best_match = file
306+
307+
# Use match if similar enough (at least 80% of expected length matched)
308+
min_score = len(expected_base) * 0.8
309+
if best_match and best_score >= min_score:
310+
actual_file = os.path.join(directory, best_match)
311+
if actual_file != expected_file:
312+
logging.info(f"Found similar file '{best_match}' for expected '{expected_filename}' (score: {best_score})")
313+
return actual_file
314+
315+
return expected_file
316+
317+
229318
def move_files_main(old_file, new_file, base_path):
230319
"""
231320
The base function for moving files with logging\n
@@ -235,10 +324,13 @@ def move_files_main(old_file, new_file, base_path):
235324
:return: None
236325
"""
237326
if not os.path.isfile(new_file):
327+
# Try to find the file, handling minor naming discrepancies
328+
actual_old_file = find_matching_file(old_file)
329+
238330
try:
239-
shutil.move(old_file, new_file)
331+
shutil.move(actual_old_file, new_file)
240332
except Exception as error:
241-
logging.error(f"Unable to move '{old_file}' to '{base_path}' - Error: {error}")
333+
logging.error(f"Unable to move '{actual_old_file}' to '{base_path}' - Error: {error}")
242334
else:
243335
logging.info(f"File: {new_file} already exists. Not moving.")
244336

0 commit comments

Comments
 (0)