Skip to content

Commit 0148cf3

Browse files
committed
TrackMagic2 working prototype
0 parents  commit 0148cf3

19 files changed

+835
-0
lines changed

TrackMagic.exe

180 KB
Binary file not shown.

Uninstall.exe

138 KB
Binary file not shown.

readme.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# TrackMagic: YouTube downloader for personal use
2+
### Why do I need this thing
3+
Idk maybe u want to download a few videos to keep them or smth
4+
### How it work??
5+
Open the `TrackMagic` file and follow the instructions by giving a video/playlist link and the application will start downloading from youtube.
6+
The output folders are `./Video/` and `./Audio/`
7+
8+
## Setup this thing
9+
### First of all, what do you need?
10+
- You need a working `Python interpreter`
11+
- And you need `ffmpeg`
12+
13+
To setup literally just open `TrackMagic` and it will setup itself.
14+
### You need Python
15+
If you're met with the message \
16+
`'py' is not recognized as an internal or external command,` \
17+
`operable program or batch file.` \
18+
Install a [Python Interpreter](https://www.python.org/downloads/)
19+
### If that doesn't work, then you might need be missing `pip` from the PATH
20+
Maybe you're asking questions like... wtf is pip and why are you screaming PATH. \
21+
Bro, just don't question it..
22+
### How to pip if has no pip
23+
1. Install python if not already installed. \
24+
1.1 If you don't have python and you're installing, then enable the option `Add to %PATH%` and you will get the pip.
25+
2. Now if you already had python and pip is missing, head over to windows search and type `Edit the system enviroment variables`.
26+
3. Click the first result that shows up and in the next window select `Enviroment Variables...`.
27+
4. Another window should appear, now just click `Path` or `PATH` in the `User variables for X` section.
28+
5. Select the option `Edit` and another window should appear.
29+
6. Now select `New` and we will have to add the path to the directory that contains `pip.exe`. \
30+
6.1 Head over to `C:\Users\%USERNAME%\AppData\Local\Programs\Python`. \
31+
6.2 Find the python version that you just installed and enter the `Scripts` folder. \
32+
6.3 Copy the path to this directory, should look something like `C:\Users\%USERNAME%\AppData\Local\Programs\Python\PythonYOURVERSION\Scripts`
33+
7. Now you should have pip added, you can exit all the windows by clicking `Ok`.
34+
### If that doesn't work, then you might need be missing `ffmpeg` from the PATH
35+
Basically download [ffmpeg for windows](https://www.gyan.dev/ffmpeg/builds/) and add it to path like in the pip tutorial above ^ ^ ^ \
36+
It's recommended to download the `ffmpeg full` version(haven't tested the essentials version)
37+
### If it still doesn't work, it's ggs
38+
Well whatever it is you can try messaging the creator with the logs files (good luck)

requirements.txt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
PyGetWindow
2+
pywin32
3+
yt-dlp
4+
Pillow
5+
ffmpy

trackmagic_src/TrackMagic.bat

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@echo off
2+
cls
3+
py TrackMagic.py
4+
pause

trackmagic_src/TrackMagic.py

+355
Large diffs are not rendered by default.

trackmagic_src/Uninstall.bat

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
echo off
2+
cls
3+
pip uninstall -r ..\requirements.txt -y
4+
echo Everything has been cleaned up!
5+
pause

trackmagic_src/classes/Logger.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from datetime import datetime
2+
from time import time
3+
import traceback
4+
import os
5+
6+
LOGS_DIRECTORY = '../logs/'
7+
8+
9+
class Logger:
10+
log_file_extension = 'txt'
11+
12+
def __init__(self, log_file_name: str, log_to_file: bool = False):
13+
self.log_file_name: str = Logger.rename_file_if_exists(log_file_name, Logger.log_file_extension)
14+
self.log_to_file: bool = log_to_file
15+
16+
def set_log_to_file(self, state: bool) -> None:
17+
self.log_to_file = state
18+
19+
def run(self, func) -> None:
20+
try:
21+
func()
22+
except Exception as e:
23+
error_string = traceback.format_exc()
24+
self.log('ERROR', f'An error has occurred: {e}\nLog written to {self.log_file_name}\n{error_string}')
25+
except KeyboardInterrupt:
26+
self.log('EXIT', 'Closed via keyboard interrupt')
27+
28+
def log(self, subsection: str = 'NORMAL', text: str = '') -> None:
29+
log_text = f'[{datetime.now().strftime("%m/%d/%Y %H:%M:%S")}][{subsection}] {text}\n'
30+
print(log_text, end='')
31+
32+
if self.log_to_file:
33+
if not os.path.exists(LOGS_DIRECTORY):
34+
os.mkdir(LOGS_DIRECTORY)
35+
with open(f'{LOGS_DIRECTORY}{self.log_file_name}', 'a', encoding='utf-8') as log_file:
36+
log_file.write(log_text)
37+
38+
@staticmethod
39+
def rename_file_if_exists(file_name: str, file_extension: str) -> str:
40+
file_number = 0
41+
search_file = '.'.join([file_name, file_extension])
42+
while os.path.exists(search_file):
43+
file_number += 1
44+
search_file = f'{file_name} {file_number}{file_extension}'
45+
return search_file
46+
47+
48+
class Launcher:
49+
def __init__(self):
50+
self.startup_date_timestamp: datetime = datetime.now()
51+
self.startup_timestamp: float = time()
52+
53+
self.startup_timestamp_int: int = int(self.startup_timestamp)
54+
self.logger = Logger(f'{datetime.now().strftime("%Y-%m-%d %H-%M-%S")}')
55+
56+
def launch(self, launch_main) -> None:
57+
self.logger.run(launch_main)

trackmagic_src/classes/Records.py

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from classes.static.Configuration import Configuration
2+
from classes.static.FileExplorer import FileExplorer
3+
from classes.Logger import Logger
4+
import os
5+
6+
7+
class Records:
8+
def __init__(self, logger: Logger):
9+
self.logger = logger
10+
self.records: dict = {}
11+
12+
def check_integrity(self) -> None:
13+
for record in self.records.values():
14+
record.check_file_integrity()
15+
self.save_records()
16+
17+
def load_records(self) -> None:
18+
content = FileExplorer.read_or_create_empty(Configuration.records_file)
19+
records_unparsed = [attributes for attributes in content.split(Configuration.record_seperator)]
20+
21+
for attributes in records_unparsed:
22+
record = Record()
23+
if record.parse(attributes):
24+
self.records[record.video_id] = record
25+
26+
self.logger.log('Records', f'Loaded {len(self.records)} record/s')
27+
28+
def save_records(self) -> None:
29+
new_content = ''
30+
for record in self.records.values():
31+
new_content += record.serialize()
32+
33+
with open(Configuration.records_file, 'w', encoding='utf-8') as f:
34+
f.write(new_content)
35+
36+
def get_record(self, video_id: str):
37+
return self.records.get(video_id, None)
38+
39+
def update_record(self, record):
40+
video_id: str = record.video_id
41+
self.records[video_id] = record
42+
43+
44+
class Record:
45+
def __init__(self):
46+
self.video_id: str = None # type: ignore
47+
self.title: str = None # type: ignore
48+
self.length: int = None # type: ignore
49+
self.video: str = None # type: ignore
50+
self.video_stream: str = None # type: ignore
51+
self.audio: str = None # type: ignore
52+
self.audio_stream: str = None # type: ignore
53+
self.thumbnail: str = None # type: ignore
54+
55+
def check_file_integrity(self) -> bool:
56+
changed_data = False
57+
if self.video is not None and not os.path.exists(self.video):
58+
self.video = None
59+
self.video_stream = None
60+
changed_data = True
61+
62+
if self.audio is not None and not os.path.exists(self.audio):
63+
self.audio = None
64+
self.audio_stream = None
65+
changed_data = True
66+
67+
return changed_data
68+
69+
def serialize(self) -> str:
70+
return '\n'.join(f'{key}={item}' for key, item in self.__dict__.items()) + '\n' + Configuration.record_seperator
71+
72+
def parse(self, unparsed_attributes: str) -> bool:
73+
attributes = [attribute.split('=') for attribute in unparsed_attributes.splitlines(keepends=False)]
74+
75+
if not attributes:
76+
return False # Drop bad records (empty lines, corrupted, etc.)
77+
78+
for key, value in attributes:
79+
if value == 'None':
80+
value = None
81+
elif value == 'False':
82+
value = False
83+
elif value == 'True':
84+
value = True
85+
elif key == 'length' and value is not None:
86+
value = int(value)
87+
setattr(self, key, value)
88+
return True
89+
90+
def __repr__(self):
91+
return str(self.__dict__)

trackmagic_src/classes/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class Configuration:
2+
records_file: str = '../records'
3+
record_seperator: str = '-= End of record =-\n'
4+
video_dir: str = '../Videos/'
5+
audio_dir: str = '../Audio/'
6+
temp_dir: str = '../temp/'
7+
audio_ext: str = '.m4a'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from classes.static.Configuration import Configuration
2+
import yt_dlp as youtube
3+
import os
4+
5+
6+
class Downloader:
7+
stream_filepath: str = None # type: ignore
8+
stream_filename: str = None # type: ignore
9+
10+
@staticmethod
11+
def _set_stream_filepath_hook(data: dict) -> None:
12+
if data['status'] == 'finished':
13+
Downloader.stream_filepath = data['info_dict']['filename']
14+
Downloader.stream_filename = os.path.basename(Downloader.stream_filepath)
15+
16+
@staticmethod
17+
def download_stream(format_id: str, video_url: str) -> str:
18+
downloader_options = {'outtmpl': f'{Configuration.temp_dir}%(title)s.%(ext)s',
19+
'progress_hooks': [Downloader._set_stream_filepath_hook],
20+
'format': format_id,
21+
'noplaylist': True,
22+
'quiet': True}
23+
24+
with youtube.YoutubeDL(downloader_options) as ydl:
25+
ydl.download(video_url)
26+
27+
return Downloader.stream_filepath
+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from classes.static.Configuration import Configuration
2+
from classes.Logger import Logger
3+
import subprocess
4+
import ffmpy
5+
import os
6+
7+
8+
class FFmpeg:
9+
logger: Logger = None # type: ignore
10+
11+
@staticmethod
12+
def init(logger: Logger) -> None:
13+
FFmpeg.logger = logger
14+
15+
@staticmethod
16+
def check_ffmpeg():
17+
try:
18+
ffmpeg_env = os.getenv('FFMPEG_BINARY', 'ffmpeg')
19+
exit_code = subprocess.call([ffmpeg_env, '-version', '-loglevel panic'], stdout=subprocess.DEVNULL)
20+
except FileNotFoundError:
21+
FFmpeg.logger.log('FFmpeg', 'FFmpeg was not found on your system')
22+
FFmpeg.logger.log('FFmpeg', 'Either you\'ve never downloaded FFmpeg')
23+
FFmpeg.logger.log('FFmpeg', ' or you haven\'t added it to PATH')
24+
FFmpeg.logger.log('FFmpeg', '')
25+
FFmpeg.logger.log('FFmpeg', 'Check readme.md for more information')
26+
exit(-1)
27+
28+
# @staticmethod
29+
# def process_video_stream(video_stream, progressive: bool):
30+
# video_at = video_stream.download(Configuration.temp_video_dir)
31+
# video_file = os.path.basename(video_at)
32+
# video_filename, video_ext = os.path.splitext(video_file)
33+
# if not progressive:
34+
# return video_at
35+
#
36+
# video_path = f'{Configuration.video_dir}{video_filename}.mp4'
37+
# ffmpeg = ffmpy.FFmpeg(inputs={video_at: None},
38+
# outputs={video_path: None},
39+
# global_options='-y -loglevel warning')
40+
# ffmpeg.run()
41+
# os.remove(video_at)
42+
# return video_path
43+
44+
@staticmethod
45+
def process_audio_from_video(video_path: str):
46+
video_directory = os.path.abspath(video_path)
47+
video_file = os.path.basename(video_path)
48+
video_filename, _ = os.path.splitext(video_file)
49+
50+
audio_path = os.path.join(video_directory, f'{video_filename}.{Configuration.audio_ext}')
51+
52+
ffmpeg = ffmpy.FFmpeg(inputs={video_path: None},
53+
outputs={audio_path: None},
54+
global_options='-y -loglevel warning')
55+
ffmpeg.run()
56+
return audio_path
57+
58+
@staticmethod
59+
def convert_audio(audio_path: str):
60+
audio_directory = os.path.dirname(audio_path)
61+
audio_file = os.path.basename(audio_path)
62+
audio_filename, audio_extension = os.path.splitext(audio_file)
63+
64+
if audio_extension == Configuration.audio_ext:
65+
return audio_path
66+
67+
new_audio_path = os.path.join(audio_directory, f'{audio_filename}{Configuration.audio_ext}')
68+
69+
ffmpeg = ffmpy.FFmpeg(inputs={audio_path: None},
70+
outputs={new_audio_path: None},
71+
global_options='-y -loglevel warning')
72+
ffmpeg.run()
73+
return new_audio_path
74+
75+
@staticmethod
76+
def merge_video_audio(video_path: str, audio_path: str):
77+
video_directory = os.path.dirname(video_path)
78+
video_file = os.path.basename(video_path)
79+
video_filename, _ = os.path.splitext(video_file)
80+
new_video_path = os.path.join(video_directory, f'{video_filename}.merge.mp4')
81+
82+
ffmpeg = ffmpy.FFmpeg(inputs={video_path: None, audio_path: None},
83+
outputs={new_video_path: '-c copy -map 0:v:0 -map 1:a:0'},
84+
global_options='-y -loglevel warning')
85+
ffmpeg.run()
86+
return new_video_path
87+
88+
@staticmethod
89+
def add_thumbnail_to_media_file(media_path: str, thumbnail_path: str): # Either video or audio (apparently)
90+
if thumbnail_path is None:
91+
return media_path
92+
93+
media_directory = os.path.dirname(media_path)
94+
media_file = os.path.basename(media_path)
95+
media_filename, media_extension = os.path.splitext(media_file)
96+
new_media_path = os.path.join(media_directory, f'{media_filename}.thumbnail{media_extension}')
97+
98+
try:
99+
ffmpeg = ffmpy.FFmpeg(inputs={media_path: None, thumbnail_path: None},
100+
outputs={new_media_path: '-map 1 -map 0 -c copy -disposition:0 attached_pic'},
101+
global_options='-y -loglevel warning')
102+
ffmpeg.run()
103+
except ffmpy.FFRuntimeError:
104+
return media_path
105+
106+
return new_media_path

0 commit comments

Comments
 (0)