Skip to content

Commit

Permalink
TrackMagic2 working prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
Matqyou committed Aug 25, 2024
0 parents commit 0148cf3
Show file tree
Hide file tree
Showing 19 changed files with 835 additions and 0 deletions.
Binary file added TrackMagic.exe
Binary file not shown.
Binary file added Uninstall.exe
Binary file not shown.
38 changes: 38 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# TrackMagic: YouTube downloader for personal use
### Why do I need this thing
Idk maybe u want to download a few videos to keep them or smth
### How it work??
Open the `TrackMagic` file and follow the instructions by giving a video/playlist link and the application will start downloading from youtube.
The output folders are `./Video/` and `./Audio/`

## Setup this thing
### First of all, what do you need?
- You need a working `Python interpreter`
- And you need `ffmpeg`

To setup literally just open `TrackMagic` and it will setup itself.
### You need Python
If you're met with the message \
`'py' is not recognized as an internal or external command,` \
`operable program or batch file.` \
Install a [Python Interpreter](https://www.python.org/downloads/)
### If that doesn't work, then you might need be missing `pip` from the PATH
Maybe you're asking questions like... wtf is pip and why are you screaming PATH. \
Bro, just don't question it..
### How to pip if has no pip
1. Install python if not already installed. \
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.
2. Now if you already had python and pip is missing, head over to windows search and type `Edit the system enviroment variables`.
3. Click the first result that shows up and in the next window select `Enviroment Variables...`.
4. Another window should appear, now just click `Path` or `PATH` in the `User variables for X` section.
5. Select the option `Edit` and another window should appear.
6. Now select `New` and we will have to add the path to the directory that contains `pip.exe`. \
6.1 Head over to `C:\Users\%USERNAME%\AppData\Local\Programs\Python`. \
6.2 Find the python version that you just installed and enter the `Scripts` folder. \
6.3 Copy the path to this directory, should look something like `C:\Users\%USERNAME%\AppData\Local\Programs\Python\PythonYOURVERSION\Scripts`
7. Now you should have pip added, you can exit all the windows by clicking `Ok`.
### If that doesn't work, then you might need be missing `ffmpeg` from the PATH
Basically download [ffmpeg for windows](https://www.gyan.dev/ffmpeg/builds/) and add it to path like in the pip tutorial above ^ ^ ^ \
It's recommended to download the `ffmpeg full` version(haven't tested the essentials version)
### If it still doesn't work, it's ggs
Well whatever it is you can try messaging the creator with the logs files (good luck)
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
PyGetWindow
pywin32
yt-dlp
Pillow
ffmpy
4 changes: 4 additions & 0 deletions trackmagic_src/TrackMagic.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@echo off
cls
py TrackMagic.py
pause
355 changes: 355 additions & 0 deletions trackmagic_src/TrackMagic.py

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions trackmagic_src/Uninstall.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
echo off
cls
pip uninstall -r ..\requirements.txt -y
echo Everything has been cleaned up!
pause
57 changes: 57 additions & 0 deletions trackmagic_src/classes/Logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from datetime import datetime
from time import time
import traceback
import os

LOGS_DIRECTORY = '../logs/'


class Logger:
log_file_extension = 'txt'

def __init__(self, log_file_name: str, log_to_file: bool = False):
self.log_file_name: str = Logger.rename_file_if_exists(log_file_name, Logger.log_file_extension)
self.log_to_file: bool = log_to_file

def set_log_to_file(self, state: bool) -> None:
self.log_to_file = state

def run(self, func) -> None:
try:
func()
except Exception as e:
error_string = traceback.format_exc()
self.log('ERROR', f'An error has occurred: {e}\nLog written to {self.log_file_name}\n{error_string}')
except KeyboardInterrupt:
self.log('EXIT', 'Closed via keyboard interrupt')

def log(self, subsection: str = 'NORMAL', text: str = '') -> None:
log_text = f'[{datetime.now().strftime("%m/%d/%Y %H:%M:%S")}][{subsection}] {text}\n'
print(log_text, end='')

if self.log_to_file:
if not os.path.exists(LOGS_DIRECTORY):
os.mkdir(LOGS_DIRECTORY)
with open(f'{LOGS_DIRECTORY}{self.log_file_name}', 'a', encoding='utf-8') as log_file:
log_file.write(log_text)

@staticmethod
def rename_file_if_exists(file_name: str, file_extension: str) -> str:
file_number = 0
search_file = '.'.join([file_name, file_extension])
while os.path.exists(search_file):
file_number += 1
search_file = f'{file_name} {file_number}{file_extension}'
return search_file


class Launcher:
def __init__(self):
self.startup_date_timestamp: datetime = datetime.now()
self.startup_timestamp: float = time()

self.startup_timestamp_int: int = int(self.startup_timestamp)
self.logger = Logger(f'{datetime.now().strftime("%Y-%m-%d %H-%M-%S")}')

def launch(self, launch_main) -> None:
self.logger.run(launch_main)
91 changes: 91 additions & 0 deletions trackmagic_src/classes/Records.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from classes.static.Configuration import Configuration
from classes.static.FileExplorer import FileExplorer
from classes.Logger import Logger
import os


class Records:
def __init__(self, logger: Logger):
self.logger = logger
self.records: dict = {}

def check_integrity(self) -> None:
for record in self.records.values():
record.check_file_integrity()
self.save_records()

def load_records(self) -> None:
content = FileExplorer.read_or_create_empty(Configuration.records_file)
records_unparsed = [attributes for attributes in content.split(Configuration.record_seperator)]

for attributes in records_unparsed:
record = Record()
if record.parse(attributes):
self.records[record.video_id] = record

self.logger.log('Records', f'Loaded {len(self.records)} record/s')

def save_records(self) -> None:
new_content = ''
for record in self.records.values():
new_content += record.serialize()

with open(Configuration.records_file, 'w', encoding='utf-8') as f:
f.write(new_content)

def get_record(self, video_id: str):
return self.records.get(video_id, None)

def update_record(self, record):
video_id: str = record.video_id
self.records[video_id] = record


class Record:
def __init__(self):
self.video_id: str = None # type: ignore
self.title: str = None # type: ignore
self.length: int = None # type: ignore
self.video: str = None # type: ignore
self.video_stream: str = None # type: ignore
self.audio: str = None # type: ignore
self.audio_stream: str = None # type: ignore
self.thumbnail: str = None # type: ignore

def check_file_integrity(self) -> bool:
changed_data = False
if self.video is not None and not os.path.exists(self.video):
self.video = None
self.video_stream = None
changed_data = True

if self.audio is not None and not os.path.exists(self.audio):
self.audio = None
self.audio_stream = None
changed_data = True

return changed_data

def serialize(self) -> str:
return '\n'.join(f'{key}={item}' for key, item in self.__dict__.items()) + '\n' + Configuration.record_seperator

def parse(self, unparsed_attributes: str) -> bool:
attributes = [attribute.split('=') for attribute in unparsed_attributes.splitlines(keepends=False)]

if not attributes:
return False # Drop bad records (empty lines, corrupted, etc.)

for key, value in attributes:
if value == 'None':
value = None
elif value == 'False':
value = False
elif value == 'True':
value = True
elif key == 'length' and value is not None:
value = int(value)
setattr(self, key, value)
return True

def __repr__(self):
return str(self.__dict__)
Empty file.
7 changes: 7 additions & 0 deletions trackmagic_src/classes/static/Configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Configuration:
records_file: str = '../records'
record_seperator: str = '-= End of record =-\n'
video_dir: str = '../Videos/'
audio_dir: str = '../Audio/'
temp_dir: str = '../temp/'
audio_ext: str = '.m4a'
27 changes: 27 additions & 0 deletions trackmagic_src/classes/static/Downloader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from classes.static.Configuration import Configuration
import yt_dlp as youtube
import os


class Downloader:
stream_filepath: str = None # type: ignore
stream_filename: str = None # type: ignore

@staticmethod
def _set_stream_filepath_hook(data: dict) -> None:
if data['status'] == 'finished':
Downloader.stream_filepath = data['info_dict']['filename']
Downloader.stream_filename = os.path.basename(Downloader.stream_filepath)

@staticmethod
def download_stream(format_id: str, video_url: str) -> str:
downloader_options = {'outtmpl': f'{Configuration.temp_dir}%(title)s.%(ext)s',
'progress_hooks': [Downloader._set_stream_filepath_hook],
'format': format_id,
'noplaylist': True,
'quiet': True}

with youtube.YoutubeDL(downloader_options) as ydl:
ydl.download(video_url)

return Downloader.stream_filepath
106 changes: 106 additions & 0 deletions trackmagic_src/classes/static/FFmpeg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from classes.static.Configuration import Configuration
from classes.Logger import Logger
import subprocess
import ffmpy
import os


class FFmpeg:
logger: Logger = None # type: ignore

@staticmethod
def init(logger: Logger) -> None:
FFmpeg.logger = logger

@staticmethod
def check_ffmpeg():
try:
ffmpeg_env = os.getenv('FFMPEG_BINARY', 'ffmpeg')
exit_code = subprocess.call([ffmpeg_env, '-version', '-loglevel panic'], stdout=subprocess.DEVNULL)
except FileNotFoundError:
FFmpeg.logger.log('FFmpeg', 'FFmpeg was not found on your system')
FFmpeg.logger.log('FFmpeg', 'Either you\'ve never downloaded FFmpeg')
FFmpeg.logger.log('FFmpeg', ' or you haven\'t added it to PATH')
FFmpeg.logger.log('FFmpeg', '')
FFmpeg.logger.log('FFmpeg', 'Check readme.md for more information')
exit(-1)

# @staticmethod
# def process_video_stream(video_stream, progressive: bool):
# video_at = video_stream.download(Configuration.temp_video_dir)
# video_file = os.path.basename(video_at)
# video_filename, video_ext = os.path.splitext(video_file)
# if not progressive:
# return video_at
#
# video_path = f'{Configuration.video_dir}{video_filename}.mp4'
# ffmpeg = ffmpy.FFmpeg(inputs={video_at: None},
# outputs={video_path: None},
# global_options='-y -loglevel warning')
# ffmpeg.run()
# os.remove(video_at)
# return video_path

@staticmethod
def process_audio_from_video(video_path: str):
video_directory = os.path.abspath(video_path)
video_file = os.path.basename(video_path)
video_filename, _ = os.path.splitext(video_file)

audio_path = os.path.join(video_directory, f'{video_filename}.{Configuration.audio_ext}')

ffmpeg = ffmpy.FFmpeg(inputs={video_path: None},
outputs={audio_path: None},
global_options='-y -loglevel warning')
ffmpeg.run()
return audio_path

@staticmethod
def convert_audio(audio_path: str):
audio_directory = os.path.dirname(audio_path)
audio_file = os.path.basename(audio_path)
audio_filename, audio_extension = os.path.splitext(audio_file)

if audio_extension == Configuration.audio_ext:
return audio_path

new_audio_path = os.path.join(audio_directory, f'{audio_filename}{Configuration.audio_ext}')

ffmpeg = ffmpy.FFmpeg(inputs={audio_path: None},
outputs={new_audio_path: None},
global_options='-y -loglevel warning')
ffmpeg.run()
return new_audio_path

@staticmethod
def merge_video_audio(video_path: str, audio_path: str):
video_directory = os.path.dirname(video_path)
video_file = os.path.basename(video_path)
video_filename, _ = os.path.splitext(video_file)
new_video_path = os.path.join(video_directory, f'{video_filename}.merge.mp4')

ffmpeg = ffmpy.FFmpeg(inputs={video_path: None, audio_path: None},
outputs={new_video_path: '-c copy -map 0:v:0 -map 1:a:0'},
global_options='-y -loglevel warning')
ffmpeg.run()
return new_video_path

@staticmethod
def add_thumbnail_to_media_file(media_path: str, thumbnail_path: str): # Either video or audio (apparently)
if thumbnail_path is None:
return media_path

media_directory = os.path.dirname(media_path)
media_file = os.path.basename(media_path)
media_filename, media_extension = os.path.splitext(media_file)
new_media_path = os.path.join(media_directory, f'{media_filename}.thumbnail{media_extension}')

try:
ffmpeg = ffmpy.FFmpeg(inputs={media_path: None, thumbnail_path: None},
outputs={new_media_path: '-map 1 -map 0 -c copy -disposition:0 attached_pic'},
global_options='-y -loglevel warning')
ffmpeg.run()
except ffmpy.FFRuntimeError:
return media_path

return new_media_path
Loading

0 comments on commit 0148cf3

Please sign in to comment.