243 lines
6.6 KiB
Python
Executable File
243 lines
6.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Copyright (C) 2023 Julian Mutter (julian.mutter@comumail.de)
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import argparse
|
|
import subprocess
|
|
import os
|
|
from datetime import datetime
|
|
from playsound import playsound
|
|
import time
|
|
import re
|
|
import shutil
|
|
|
|
DEFAULT_DVD_DEVICE = "/dev/cdrom"
|
|
TMP_DIR = "tmp"
|
|
RIPPED_DIR = "ripped"
|
|
LOGFILE = "rip.log"
|
|
NOTIFICATION_SOUND = "bell.oga"
|
|
|
|
SERIES_TITLE_REGEX = r"S\d+[ _]?E(\d+)(-(\d+))?$"
|
|
MAX_FILENAME_LEN_IN_TMP = 25 # dvdbackup cuts name of output files at 33 chars
|
|
WAIT_FOR_DEVICE_TIME_SECONDS = 3
|
|
|
|
|
|
def main():
|
|
program_start_time = time.time()
|
|
args = parse_args()
|
|
|
|
if args.type == "series":
|
|
validate_series_title(args.title)
|
|
chdir_to_script_dir()
|
|
mkdirs()
|
|
|
|
dst = os.path.join(RIPPED_DIR, args.type, args.title)
|
|
if os.path.exists(dst):
|
|
print(
|
|
f"A {args.type} with this name has already been ripped. Are you sure you spelled the name right?"
|
|
)
|
|
exit(1)
|
|
|
|
if args.wait:
|
|
wait_for_dev_to_exist(args)
|
|
wait_for_dev_to_be_readable(args)
|
|
|
|
success = rip_to_tmp_dir(args)
|
|
program_execution_time_str = get_program_execution_time_str(program_start_time)
|
|
if success:
|
|
mv_ripped_from_tmp_to_ripped_dir(args)
|
|
write_to_logfile(args, "Success")
|
|
notify_ripping_success(args, program_execution_time_str)
|
|
else:
|
|
write_to_logfile(args, "FAILURE")
|
|
notify_ripping_error(args, program_execution_time_str)
|
|
|
|
print("Deleting tmp directory")
|
|
delete_tmp_dir()
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(description="Rip content of dvds")
|
|
parser.add_argument(
|
|
"type",
|
|
choices=("movie", "series"),
|
|
help="If the dvd contains one movie, or multiple episodes of a series",
|
|
)
|
|
parser.add_argument(
|
|
"title",
|
|
help='The title of the movie. Series must end with "Sx Ex-x". E.g.: "Lost S01 E1-04" or "Lost_S2_E2"',
|
|
)
|
|
parser.add_argument(
|
|
"--dev",
|
|
help=f"Dvd device to rip from. Defaults to {DEFAULT_DVD_DEVICE}",
|
|
default=DEFAULT_DVD_DEVICE,
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--wait", help=f"Wait for dvd device to exist.", action="store_true"
|
|
)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
def validate_series_title(title):
|
|
if not is_series_title_valid(title):
|
|
print("Invalid series title!")
|
|
exit(1)
|
|
|
|
|
|
def is_series_title_valid(title):
|
|
try:
|
|
episode_matches = re.search(SERIES_TITLE_REGEX, title)
|
|
if episode_matches is None:
|
|
return False
|
|
episode_from = int(episode_matches.group(1))
|
|
episode_to = episode_matches.group(3)
|
|
if episode_to:
|
|
episode_to = int(episode_to)
|
|
else:
|
|
episode_to = episode_from
|
|
|
|
if episode_from <= episode_to:
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
def chdir_to_script_dir():
|
|
os.chdir(os.path.dirname(__file__))
|
|
|
|
|
|
def mkdirs():
|
|
os.makedirs(TMP_DIR, exist_ok=True)
|
|
os.makedirs(RIPPED_DIR, exist_ok=True)
|
|
os.makedirs(os.path.join(RIPPED_DIR, "movie"), exist_ok=True)
|
|
os.makedirs(os.path.join(RIPPED_DIR, "series"), exist_ok=True)
|
|
|
|
|
|
def write_to_logfile(args, tag):
|
|
date = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
|
|
log_line = f'{date} - {args.type} - "{args.title}" - {tag}'
|
|
|
|
with open(LOGFILE, "a") as file:
|
|
file.write(log_line + "\n")
|
|
|
|
|
|
def wait_for_dev_to_exist(args):
|
|
while not os.path.exists(args.dev):
|
|
print(
|
|
f"Device {args.dev} not found. Waiting {WAIT_FOR_DEVICE_TIME_SECONDS} sec..."
|
|
)
|
|
time.sleep(WAIT_FOR_DEVICE_TIME_SECONDS)
|
|
|
|
|
|
def wait_for_dev_to_be_readable(args):
|
|
while not is_dvd_readable(args):
|
|
print(
|
|
f"Device {args.dev} found, but not readable. Waiting {WAIT_FOR_DEVICE_TIME_SECONDS} sec..."
|
|
)
|
|
time.sleep(WAIT_FOR_DEVICE_TIME_SECONDS)
|
|
|
|
|
|
def is_dvd_readable(args):
|
|
command = create_info_command(args)
|
|
proc = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
return proc.returncode != 255
|
|
|
|
|
|
def rip_to_tmp_dir(args) -> bool:
|
|
"""Returns: success of command"""
|
|
command = create_rip_command(args, TMP_DIR)
|
|
proc = subprocess.run(command)
|
|
|
|
return proc.returncode == 0
|
|
|
|
|
|
def create_info_command(args):
|
|
return ["dvdbackup", "-i", args.dev, "-I"]
|
|
|
|
|
|
def create_rip_command(args, dest):
|
|
shortened_title = shorten_title(args)
|
|
return [
|
|
"dvdbackup",
|
|
"-v",
|
|
"-p",
|
|
"-i",
|
|
args.dev,
|
|
"-o",
|
|
dest,
|
|
"-M",
|
|
"-n",
|
|
shortened_title,
|
|
]
|
|
|
|
|
|
def get_program_execution_time_str(program_start_time):
|
|
program_execution_time_minutes = (time.time() - program_start_time) / 60.0
|
|
program_execution_time_minutes = max(0.0, program_execution_time_minutes)
|
|
return f"{program_execution_time_minutes:.1f} minutes"
|
|
|
|
|
|
def notify_ripping_success(args, program_execution_time_str):
|
|
print(f"Success! Ripping took {program_execution_time_str}")
|
|
send_notification(
|
|
f'{args.type.capitalize()} "{args.title}" ripped successfully in {program_execution_time_str}!'
|
|
)
|
|
playsound(NOTIFICATION_SOUND)
|
|
|
|
|
|
def notify_ripping_error(args, program_execution_time_str):
|
|
print(f"Ripping failure after {program_execution_time_str}")
|
|
send_notification(
|
|
f'Error ripping {args.type.capitalize()} "{args.title}" after {program_execution_time_str}!'
|
|
)
|
|
playsound(NOTIFICATION_SOUND)
|
|
|
|
|
|
def send_notification(text):
|
|
subprocess.run(
|
|
["notify-send", text],
|
|
shell=False,
|
|
)
|
|
|
|
|
|
def mv_ripped_from_tmp_to_ripped_dir(args):
|
|
shortened_title = shorten_title(args)
|
|
src = os.path.join(TMP_DIR, shortened_title)
|
|
dst = os.path.join(RIPPED_DIR, args.type, args.title)
|
|
|
|
shutil.move(src, dst)
|
|
|
|
|
|
def shorten_title(args):
|
|
return args.title[-MAX_FILENAME_LEN_IN_TMP:]
|
|
|
|
|
|
def delete_tmp_dir():
|
|
try:
|
|
os.rmdir(os.path.join(TMP_DIR))
|
|
except FileNotFoundError:
|
|
pass
|
|
except OSError:
|
|
print("Failed deleting due to OSError")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|