commit 8bf94aaff150db02052591ff3ef7794ef59b3e04
parent d6eeb385e65ea2a8a552b6620bc5450fc7d12b27
Author: gearsix <gearsix@tuta.io>
Date:   Fri, 25 Nov 2022 16:20:18 +0000
v1.0.0 release; Squashed commit of the following:
commit e132740e7229762c457312bc37b206fa46496e73
Author: gearsix <gearsix@tuta.io>
Date:   Thu Nov 24 12:24:41 2022 +0000
    minor adjustments in pyqt
    - options window is moved on show
    - added spacer in center of options
    - removed sizing from options
commit c189192efb5777efe5824f7146f62110ba8bae8e
Author: gearsix <gearsix@tuta.io>
Date:   Sat Nov 19 18:27:33 2022 +0000
    sya.py: added weight system to handle multiple timestamps on a line
    Now the correct timestamp should be picked if there are multiple in a line.
    - Only a timestamp at the start or end of a line will be chosen
    - If there is a timestamp at both the start AND the end, the weight is checked.
    Each timestamp found will add a weight to the left or right (depending where it's found).
    p.s. All subprocess calls use shell=False;
commit b0d0d528647c4cf4b74107924d70a6db97b268db
Author: gearsix <gearsix@tuta.io>
Date:   Fri Nov 18 23:04:18 2022 +0000
    refactor: updated README.md
commit a8e5b5f63010020699b9a0f9c3e692ca4ea0945e
Author: gearsix <gearsix@tuta.io>
Date:   Fri Nov 18 22:19:35 2022 +0000
    feature: added HELP in place of the (redundant) 'Quit' button
commit 2a4237212bf3c91dc727d86f2353010940dbf2f8
Author: gearsix <gearsix@tuta.io>
Date:   Fri Nov 18 21:56:10 2022 +0000
    bugfix: realised shell=False is required on windows to avoid consoles popping up
commit 026f57a94251301c1f44afc99c5a23f4b93b0785
Author: gearsix <gearsix@tuta.io>
Date:   Fri Nov 18 21:55:37 2022 +0000
    bugfix: _init_options_output was using the wrong label
commit a778fa5d41f46c3bfbed9a706cdca64b25c43a5b
Author: gearsix <gearsix@tuta.io>
Date:   Fri Nov 18 21:40:03 2022 +0000
    bugfix: removed 'flv' format option
commit 9af98328f7729a502e9f57c304e87eb64d5863bb
Author: gearsix <gearsix@tuta.io>
Date:   Fri Nov 18 15:01:07 2022 +0000
    bugfix: ffmpeg binary assigned to wrong arg in sya-pyqt __main__
    refactor: Removed centerWidget calls, annoyingly spawns on 2nd monitor
    refactor: added NoWrap to logger, output is easier to read
commit db33381e083fbba6dba7e237c55c37ffa1dd41f4
Author: gearsix <gearsix@tuta.io>
Date:   Fri Nov 18 15:00:43 2022 +0000
    minor amendment in README
commit 5e49cf578c213d48ec2dfccc213eb4b466fef63d
Author: gearsix <gearsix@tuta.io>
Date:   Fri Nov 18 14:58:41 2022 +0000
    bugfix: subprocess calls on posix work properly now.
    refactor: created read_tracklen(), from split_tracks.
    - shell=True had to be removed
    - Also added 'Success' print before clean exit
commit 09760175daa8d39d79a265230e9a17b1538b26cd
Author: gearsix <gearsix@tuta.io>
Date:   Thu Nov 17 00:13:58 2022 +0000
    Bunch of improvements to sya-pyqt
    - The options window is now a fixed size (400,169)
    - Removed the `del self.logger` and `del self.options` from quit (unnecessary)
    - Fixed the application exiting
    - Hooked `quit` into `options.closeEvent`
    - Removed the `shutil.rmtree(self.fnSyaArgs.output)`, seemed too dangerous.
    - Removed the stretch from `sya_gui_combobox` return
commit a957bb0bdea14e1aa3be4a5c8fd57629a82106a0
Author: gearsix <gearsix@tuta.io>
Date:   Sat Nov 12 20:13:04 2022 +0000
    docs: migrated a lot of doc files to *README.md*.
commit 8119c02c7aa3702da3833381dce1ac2037402ce4
Author: gearsix <gearsix@tuta.io>
Date:   Mon Nov 7 22:51:09 2022 +0000
    minor fixes & adjustments to argument defaults
    - default yt-dlp & ffmpeg argument have '.exe' on default for windows (if not provided)
    - sya-pyqt accepts yt-dlp & ffmpeg args (without overriding for bundled)
    - bugfix to logger text cursor, now it *always* appends (even if you click to move it)
    - check_options_ok called on *any* options change
commit 0ccde1a9477b53b04d33c07b1f0f481e8d77706a
Author: gearsix <gearsix@tuta.io>
Date:   Mon Nov 7 15:25:27 2022 +0000
    bugfix: optionsOk is enabled in done, not postMain
commit 69e98b910b09eef590df3ec1470725607418395e
Author: gearsix <gearsix@tuta.io>
Date:   Mon Nov 7 15:00:59 2022 +0000
    fix: sya-smol.png was still being used as the logo icon
commit 8fbaf50552dc3144f50fb20c919921677f7efa9e
Author: gearsix <gearsix@tuta.io>
Date:   Mon Nov 7 13:29:20 2022 +0000
    updated docs
commit 28665441c9cd73f9ee0a91491a2be77d1646bb7f
Author: gearsix <gearsix@tuta.io>
Date:   Mon Nov 7 13:05:29 2022 +0000
    added sya.png logo; tracklist & output reset on 'done'
commit e19eb69125455f5dd5a1cb5ac1a081855781b086
Author: gearsix <gearsix@tuta.io>
Date:   Fri Nov 4 22:02:31 2022 +0000
    sya-pyqt: massive refactor to tidyup and make it more manageable.
    Not much changed architecturally, aside from a few renames and that `_init_combobox` and `_init_filepicker` are now their own functions . Naming was more standardised and PyQt signals are effectively used.
    Also 'log()' was removed from *sya.py* (was unnecessary) and `shell=True` was added to all the `Popen` calls to avoid a console window popping up in the sya-pyqt binary. Also (finally) fixed the string decoding.
Diffstat:
| A | BUGS.md |  |  | 29 | +++++++++++++++++++++++++++++ | 
| D | BUGS.txt |  |  | 23 | ----------------------- | 
| A | HELP.md |  |  | 65 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | README.md |  |  | 150 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| D | README.txt |  |  | 126 | ------------------------------------------------------------------------------- | 
| A | TODO.txt |  |  | 3 | +++ | 
| D | requirements.txt |  |  | 41 | ----------------------------------------- | 
| A | screenshot.PNG |  |  | 0 |  | 
| M | sya-pyqt.py |  |  | 477 | ++++++++++++++++++++++++++++++++++++++++++++++++------------------------------- | 
| A | sya.png |  |  | 0 |  | 
| M | sya.py |  |  | 110 | +++++++++++++++++++++++++++++++++++++++++++++++++------------------------------ | 
11 files changed, 607 insertions(+), 417 deletions(-)
diff --git a/BUGS.md b/BUGS.md
@@ -0,0 +1,29 @@
+# BUGS
+
+## Legend
+
+[ ] = Not started
+[x] = Won't do
+[~] = Doing
+[*] = Done
+- ... = Note about the above item
+
+## List
+
+---
+
+**2021-07-31** [ ] don't apply file numbering when song names include file number already
+
+	#regex-improvement
+
+---
+
+**2021-06-22** [*] handle multiple timestamp regex matches
+	When there are multiple matches for the timestamp regex in a line, the last match is used as the timestamp.
+
+	Discovered that the following line causes an issue because 2019 is counted as a timestamp
+	`58:18     C4C - Melted w_ Hazy Year (Chillhop Winter Essentials 2019).`
+	
+	#regex-improvement
+
+---
diff --git a/BUGS.txt b/BUGS.txt
@@ -1,23 +0,0 @@
-# BUGS
-
-## Legend
-
-[ ] = Not started
-[x] = Won't do
-[~] = Doing
-[*] = Done
-- ... = Note about the above item
-
-## List
-
-**2021-07-31** [ ] don't apply file numbering when song names include file number already
-
-	#regex-improvement
-
-**2021-06-22** [*] handle multiple timestamp regex matches
-	When there are multiple matches for the timestamp regex in a line, the last match is used as the timestamp.
-
-	Discovered that the following line causes an issue because 2019 is counted as a timestamp
-	`58:18     C4C - Melted w_ Hazy Year (Chillhop Winter Essentials 2019).`
-	
-	#regex-improvemnet
diff --git a/HELP.md b/HELP.md
@@ -0,0 +1,65 @@
+
+# Help
+
+**sya - split youtube audio**, downloads, converts & splits audio from youtube videos into multiple audio tracks.
+
+## Overview
+
+To work sya requires some manual work: **tracklist** information. For more details on this, see **Tracklists** below.<br/>
+The rest of the options can be configured but are provided with defaults.
+
+Here's an overview of the options:
+
+- **Tracklist** - the text file containing tracklist information
+- **Format** - set the format to convert the audio to
+- **Quality** - set the audio quality to download in, for reference 5 is equal to *128k*
+- **Keep unsplit file** - keep the downloaded audio file (before it gets split up)
+- **Output** - the directory to download the audio track to and split it into multiple tracks
+
+The resulting files can be found at the *Output:* filepath on your system.
+
+If you've found a bug or want to suggest improvements, email: `gearsix@tuta.io`
+
+## Tracklists
+
+A tracklist is just a text file some where on your system.
+It should contains:
+
+- A *youtube URL* to download the audio from, this should be on the first line.
+- Timestamps and titles for each track to split, there should be one timestamp and one title per-track (this can usually be found in the youtube video description or a top comment).
+
+**Example**
+
+Below you can see the contents of an example playlist.<br/>
+Try saving it to a text file on your computer and as a test if you like.
+
+		https://www.youtube.com/watch?v=LbjcaMAhJRQ
+		Sneaky Snitch (0:00)
+		Fluffing a Duck (2:16)
+		Cipher (3:24)
+		Scheming Weasel (7:15)
+		Carefree (8:44)
+		Thatched Villagers (12:09)
+		Monkeys Spinning Monkeys (16:15)
+		Wallpaper (18:20)
+		Pixel Peeker Polka (21:59)
+		Killing Time (25:21)
+		Hitman (28:46)
+		The Cannery (32:07)
+		Cut and Run (35:09)
+		Life of Riley (38:44)
+		Quirky Dog (42:39)
+		The Complex (45:08)
+		Hyperfun (49:35)
+		Black Vortex (53:29)
+		Rock on Chicago (56:19)
+		Volatile Reaction (57:58)
+		On the Ground (1:00:44)
+		Wagon Wheel (electronic) (1:03:23)
+		Call to Adventure (1:08:26)
+		Hustle (1:12:33)
+		Cupids Revenge (1:14:34)
+		Dirt Rhodes (1:16:20)
+		Rhinoceros (1:18:20)
+		Who Likes to Party (1:21:43)
+		Spazzmatica Polka (1:26:01)
diff --git a/README.md b/README.md
@@ -0,0 +1,150 @@
+
+# sya
+
+**sya - split youtube audio**, download, convert & split audio from youtube videos into multiple audio tracks using 'yt-dlp' and 'ffmpeg'.
+
+---
+
+## sya.py
+
+This is the base Python script, which contains all the functionality:
+
+	sya.py [OPTIONS] TRACKLIST
+
+### OPTIONS
+
+```
+  -h, --help
+    show this help message and exit
+  
+  -o [PATH], --output [PATH]
+    specify the directory to write output files to (default: ./out)
+						
+  -f [FORMAT], --format [FORMAT]
+    specify the --audio-format argument to pass to yt-dlp (default: mp3)
+	
+  -q [QUALITY], --quality [QUALITY]
+    specify the --audio-quality argument to pass to yt-dlp (default: 320K)
+
+  --yt-dlp [PATH]
+    path of the "yt-dlp" binary to use
+	
+  --ffmpeg [PATH]
+    path of the "ffmpeg" binary to use
+	
+  -k, --keep
+    keep any files removed during processing (full video/audio file)
+```
+
+### TRACKLIST
+
+**TRACKLIST** should be the filepath of a UTF-8 text file that has the URL of the video to download to audio track of on line 1.
+Every line after this should be the track name and timestamp of each track, note that the timestamp is *where the track starts*.
+
+Regex is used to parse the tracks (timestamp and name), it tries to be fairly accomodating but isn't perfect - so here are a few rules:
+
+- Timestamps can be before or after the track name.
+- Timestamps can be wrapped in `[]` or `()`.
+- Timestamps can be `MM:SS` or `HH:MM:SS`.
+- Timestamps can be split using `:` or `.`.
+
+<details>
+	<summary>Example Tracklist</summary>
+	<pre>
+	https://www.youtube.com/watch?v=LbjcaMAhJRQ
+	Sneaky Snitch (0:00)
+	Fluffing a Duck (2:16)
+	Cipher (3:24)
+	Scheming Weasel (7:15)
+	Carefree (8:44)
+	Thatched Villagers (12:09)
+	Monkeys Spinning Monkeys (16:15)
+	Wallpaper (18:20)
+	Pixel Peeker Polka (21:59)
+	Killing Time (25:21)
+	Hitman (28:46)
+	The Cannery (32:07)
+	Cut and Run (35:09)
+	Life of Riley (38:44)
+	Quirky Dog (42:39)
+	The Complex (45:08)
+	Hyperfun (49:35)
+	Black Vortex (53:29)
+	Rock on Chicago (56:19)
+	Volatile Reaction (57:58)
+	On the Ground (1:00:44)
+	Wagon Wheel (electronic) (1:03:23)
+	Call to Adventure (1:08:26)
+	Hustle (1:12:33)
+	Cupids Revenge (1:14:34)
+	Dirt Rhodes (1:16:20)
+	Rhinoceros (1:18:20)
+	Who Likes to Party (1:21:43)
+	Spazzmatica Polka (1:26:01)
+	</pre>
+</details>
+
+### Install
+
+It's a Python script, so there are many ways to install it.
+
+Here's one that should work on *any* system:
+
+	python3 ./setup.py install --user --record install.txt
+
+To **uninstall**, just remove all files recorded to *./install.txt*.
+
+---
+
+## sya-pyqt
+
+Some people don't like the cli and I wanted to play with PyQt, so sya-pyqt wraps a nice GUI around the *sya.py* runtime.
+
+
+
+### Development
+
+Working on or building requires *PyQt5*, this can be installed with `pip install PyQt5`.
+
+To run *sya-pyqt*, you don't need to build a binary everytime, you can just run `python ./sya-pyqt.py`.
+
+### Building
+
+[pyinstaller](https://pyinstaller.org) is used to build a portable binary for sya-pyqt for easy distribution.
+
+**POSIX (Linux, MacOS, BSD)**
+
+	pyinstaller ./sya-pyqt.py -F --windowed --add-data ".\HELP.md;." --add-data "./folder.png:." --add-data "./file.png:." --add-data "./sya.png:." --add-data "$FFMPEG:." --add-data "$YT-DLP:."
+
+**Windows**
+
+	pyinstaller ./sya-pyqt.py -F --windowed --add-data ".\HELP.md;." --add-data ".\folder.png;." --add-data ".\file.png;." --add-data ".\sya.png;." --add-data "$FFMPEG;." --add-data "$YT-DLP;."
+
+- Make sure you have a *yt-dlp* binary available, the filepath of this is referred to as *$YT-DLP*.
+- Make sure you have a *ffmpeg* binary available, the filepath of this is referred to as *$FFMPEG*.
+- In some cases, I've found the path of PyQt5 has had to be explicitly given to *pyinstaller*:
+`--path <site-packages filepath>\PyQt5`
+
+Optionally, you can add an icon to the binary, I'd recommend installing [Pillow](https://python-pillow.org/) so you don't need to manually convert `sya.png` to an icon file. Just add `--icon "sya.png"` to the build command.
+
+
+---
+
+## Thanks
+
+These two tools do all the heavy lifting:
+- youtube-dl (https://ytdl-org.github.io/youtube-dl/)
+- ffmpeg (https://ffmpeg.org)
+
+And the cool folder & file icons used in 'sya-pyqt' are from the Palemoon MicroMoon theme: 
+https://repo.palemoon.org/Lootyhoof/micromoon
+
+
+## Disclaimer
+
+It should go without saying, don't use this for pirating music. Get nice high-quality, properly tagged audio tracks from official services (shoutout to Bandcamp).
+The tool was originally written to download radio mixes and ambient soundtracks for a DnD group.
+
+## Authors
+
+- gearsix
diff --git a/README.txt b/README.txt
@@ -1,126 +0,0 @@
-NAME
-  sya - split youtube audio
-
-SYNOPSIS
-  sya.py [OPTIONS] TRACKLIST
-
-DESCRIPTION
-  sya downloads, converts and splits youtube videos into multiple audio
-  tracks using `youtube-dl` and `ffmpeg`.
-
-OPTIONS
-  -h --help		displays help message
-  -k, --keep	
-	youtube-dl option, keep the full track on disk after post-processing,
-	the video is erased by default
-  -o, --output [PATH]	
-	specify the directory to write output files to (defaults to a directory
-	named after the tracklist filename)
-  -f, --format [EXT]	
-	specify the --audio-format argument to pass to youtube-dl (default: mp3)
-  -q, --quality [QUALITY]	
-	specify the --audio-quality argument to pass (default: 320K)
-  --youtube-dl [PATH]	
-	path of the youtube-dl binary to use
-  --ffmpeg [PATH]	
-	path of the ffmpeg binary to use
-
-TRACKLIST
-  TRACKLIST files should be text file that has the URL/v=code of the youtube video to
-  download on the first line and the starting timestamp of each section to split, followed
-  by the title of that section section for every line after.
-
-  Of course, you don't have to put the timestamp first, sya will try and accomocodate
-  whatever syntax is used, just beware that this might cause problems.
-
-  Here's an example:
-
-	https://www.youtube.com/watch?v=LbjcaMAhJRQ
-	Sneaky Snitch (0:00)
-	Fluffing a Duck (2:16)
-	Cipher (3:24)
-	Scheming Weasel (7:15)
-	Carefree (8:44)
-	Thatched Villagers (12:09)
-	Monkeys Spinning Monkeys (16:15)
-	Wallpaper (18:20)
-	Pixel Peeker Polka (21:59)
-	Killing Time (25:21)
-	Hitman (28:46)
-	The Cannery (32:07)
-	Cut and Run (35:09)
-	Life of Riley (38:44)
-	Quirky Dog (42:39)
-	The Complex (45:08)
-	Hyperfun (49:35)
-	Black Vortex (53:29)
-	Rock on Chicago (56:19)
-	Volatile Reaction (57:58)
-	On the Ground (1:00:44)
-	Wagon Wheel (electronic) (1:03:23)
-	Call to Adventure (1:08:26)
-	Hustle (1:12:33)
-	Cupids Revenge (1:14:34)
-	Dirt Rhodes (1:16:20)
-	Rhinoceros (1:18:20)
-	Who Likes to Party (1:21:43)
-	Spazzmatica Polka (1:26:01)
-
-
-BUILDING
-  It's more convenient to build sya-pyqt into a single binary (bundled
-  with assets & libraries).
-  To do this 'pyinstaller' seems most convenient (installed via "pip
-  install pyinstaller"):
-
-	pyinstaller sya-pyqt.py -F --windowed --add-data "folder.png:." --add-data "file.png:."
-
-  On Windows, you'll need to use the following:
-
-	pyinstaller sya-pyqt.py -F --windowed --add-data "folder.png;." --add-data "file.png;."
-
-  This process also works to create binaries for the terminal version
-  of the tool (sya.py). When building that version, the "--windowed"
-  and "--add-data "folder.png:."" arguments don't need to be included.
-
-INSTALL
-  There are a lot of ways to install this tool.
-
-  For most cases, you can just download and run the distribution binaries.
-
-  FROM SOURCE	
-  This method should work on all platforms to install it for the current
-  user.
-  Note that there are a lot of ways to install a python package from
-  source, this is just one.
-
-	python3 ./setup.py install --user --record install.txt
-
-  The "--user" argument will only install sya for the current user. Remove
-  it to install the tool to the system, although note that this will require
-  admin permissions.
-
-  This will create a 'install.txt' file in the current directory. This file
-  contains all the files installed to your system. To uninstall using just
-  remove all of these files.
-
-THANKS
-  These two tools do all the heavy lifting:
-  - youtube-dl (https://ytdl-org.github.io/youtube-dl/)
-  - ffmpeg (https://ffmpeg.org)
-
-  And the cool icons used are from the MicroMoon theme for Palemoon: 
-  https://repo.palemoon.org/Lootyhoof/micromoon
-
-DISCLAIMER
-  It should go without saying, don't use this for pirating music.
-  If you do, you're a dick and you're working against whatever band/label you
-  love enough to download their product.
-
-  Service like Bandcamp (https://bandcamp.com) are great and allow you download
-  audio files of the albums you've bought, properly tagged and available for
-  re-download whenever you need.
-
-AUTHORS
-  - gearsix (gearsix@tuta.io)
-
diff --git a/TODO.txt b/TODO.txt
@@ -0,0 +1,2 @@
+- QSpacerItem should be dynamic, not static size
+- On exit, SyaGuiLogStream gets delted (runtime error?)
+\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
@@ -1,41 +0,0 @@
-altgraph==0.17.3
-Brotli==1.0.9
-build==0.9.0
-certifi==2022.9.24
-dbus-python==1.2.18
-distlib==0.3.6
-filelock==3.8.0
-meson==0.60.3
-MouseInfo==0.1.3
-mugshot==0.4.3
-mutagen==1.46.0
-packaging==21.3
-pep517==0.13.0
-pexpect==4.8.0
-platformdirs==2.5.2
-psutil==5.9.3
-ptyprocess==0.7.0
-PyAutoGUI==0.9.53
-pycairo==1.20.1
-pycryptodomex==3.15.0
-PyGetWindow==0.0.9
-PyGObject==3.42.2
-pyinstaller==5.6.2
-pyinstaller-hooks-contrib==2022.11
-PyMsgBox==1.0.9
-pyparsing==3.0.9
-pyperclip==1.8.2
-PyQt5==5.15.7
-PyQt5-Qt5==5.15.2
-PyQt5-sip==12.11.0
-PyRect==0.2.0
-PyScreeze==0.1.28
-python3-xlib==0.15
-pytweening==1.0.4
-tomli==2.0.1
-trash-cli==0.22.8.27
-ufw==0.36.1
-virtualenv==20.16.6
-virtualenv-clone==0.5.7
-websockets==10.4
-yt-dlp==2022.10.4
diff --git a/screenshot.PNG b/screenshot.PNG
Binary files differ.
diff --git a/sya-pyqt.py b/sya-pyqt.py
@@ -20,225 +20,330 @@ def resource_path(relative_path):
         base_path = os.path.abspath(".")
     return os.path.join(base_path, relative_path)
 
-def centerWidget(widget):
+
+def center_widget(widget):
     sg = qtwidg.QDesktopWidget().screenGeometry()
     wg = widget.geometry()
     return qtcore.QPoint(
         round(sg.width() / 2) - round(wg.width() / 2),
         round(sg.height() / 2) - round(wg.height() / 2))
 
-class LogStream(qtcore.QObject):
-    txt = qtcore.pyqtSignal(str)
 
-    def write(self, txt):
-        self.txt.emit(str(txt))
-
-class SyaGuiMain(qtcore.QThread):
+class SyaGuiThread(qtcore.QThread):
     def __init__(self, fn, args=None):
         super().__init__()
         self.fn = fn
         self.args = args
 
     def run(self):
-        if self.args != None:
-            self.fn(self.args)
-        else:
+        if self.args is None:
             self.fn()
+        else:
+            self.fn(self.args)
+
+
+class SyaGuiLogStream(qtcore.QObject):
+    txt = qtcore.pyqtSignal(str)
+
+    def write(self, txt):
+        self.txt.emit(str(txt))
+
+
+def sya_gui_combobox(parent, label, items, default_item, fn_update):
+    label = qtwidg.QLabel(label, parent)
+
+    combobox = qtwidg.QComboBox(parent)
+    for i in items:
+        combobox.addItem(i)
+    if default_item in items:
+        combobox.setCurrentIndex(items.index(default_item))
+    combobox.activated[str].connect(fn_update)
+
+    layout = qtwidg.QHBoxLayout()
+    layout.addWidget(label)
+    layout.addWidget(combobox)
+
+    return layout
+
+
+def sya_gui_filepicker(parent, label, fn_select, fn_update, default_value='', icon=''):
+    label = qtwidg.QLabel(label, parent)
+
+    lineEdit = qtwidg.QLineEdit(parent)
+    lineEdit.setText(default_value)
+    lineEdit.textChanged.connect(fn_update)
+
+    btnIcon = qtgui.QIcon(resource_path('{}.png'.format(icon)))
+    btn = qtwidg.QPushButton(btnIcon, '', parent)
+    btn.clicked.connect(fn_select)
+
+    layout = qtwidg.QHBoxLayout()
+    layout.addWidget(label)
+    layout.addWidget(lineEdit)
+    layout.addWidget(btn)
+
+    return layout, lineEdit
+
 
 class SyaGui(qtwidg.QMainWindow):
-    def __init__(self, fnSya, args):
+    def __init__(self, fn_sya, fn_sya_args):
         super().__init__()
 
-        self.args = args
-        self.fnSya = fnSya
+        self.fnSya = fn_sya
+        self.fnSyaArgs = fn_sya_args
+
+        self.availableFormats = ['mp3', 'wav', 'ogg', 'aac']
+        self.availableQualities = ['0 (better)', '1', '2', '3', '4', '5', '6', '7', '8', '9 (worse)']
+
+        self._init_options_value()
+        self._init_options()
+        self._init_help()
+        self._init_logger()
         
-        self._edits = {}
-        options = qtwidg.QWidget()
-        options.setWindowTitle('sya')
-        options = self._init_options(options)
-        #options.setWindowIcon(pyqt_options.QIcon(''))
-        options.move(centerWidget(options))
-        self._options = options
-
-        logs = qtwidg.QWidget()
-        logs.resize(800, 400)
-        logs = self._init_logs(logs)
-        logs.move(centerWidget(logs))
-        self._logs = logs
-
-        sys.stdout = LogStream(txt=self.log)
-        self._options.show()
-
-    def _init_options(self, options):
-        layout = qtwidg.QGridLayout()
-        # tracklist
-        self._tracklistLabel = 'Tracklist:'
-        layout.addLayout(self._init_filepicker(options, self._tracklistLabel,
-            self._filepicker_tracklist, self.args.tracklist, 'file'), 0, 0, 1, 3)
-        # formats
-        formats = ['mp3', 'flv', 'wav', 'ogg', 'aac']
-        layout.addLayout(self._init_combobox(options, 'Format:', self._set_format, formats,
-            self.args.format), 1, 0)
-        # quality
-        qualities = ['0 (better)', '1', '2', '3', '4', '5', '6', '7', '8', '9 (worse)']
-        layout.addLayout(self._init_combobox(options, 'Quality:', self._set_quality, qualities,
-            self.args.quality), 2, 0)
-        # keep
-        keep = qtwidg.QCheckBox('keep original', options)
-        if self.args.keep == True:
-            keep.setChecked(True)
-        keep.toggled.connect(self._keep_toggle, self.args.keep)
-        layout.addWidget(keep, 1, 2, 2, 1)
-        # output
-        self._outputLabel = 'Output:'
-        layout.addLayout(self._init_filepicker(options, self._outputLabel, self._filepicker_output,
-            self.args.output), 3, 0, 1, 3)
-        # quit
-        quit_btn = qtwidg.QPushButton('Quit')
-        quit_btn.clicked.connect(sys.exit)
-        layout.addWidget(quit_btn, 4, 1)
-        # ok
-        self._ok_btn = qtwidg.QPushButton('OK')
-        self._ok_btn.clicked.connect(self._ok)
-        layout.addWidget(self._ok_btn, 4, 2)
-        self._check_ok()
-
-        options.setLayout(layout)
-        return options
-
-    def _init_logs(self, logs):
+        self.options.closeEvent = self.quit
+        self.optionsHelp.clicked.connect(self.show_help)
+        self.optionsOk.clicked.connect(self.main)
+        self.help.closeEvent = self.hide_help
+        self.loggerCancel.clicked.connect(self.cancel)
+        self.loggerDone.clicked.connect(self.done)
+
+        sys.stdout = SyaGuiLogStream(txt=self.log)
+        self.running = 0
+
+    # Runtime Methods
+    def log(self, msg):
+        self.loggerTextbox.moveCursor(qtgui.QTextCursor.End)
+        self.loggerTextbox.textCursor().insertText(msg)
+        self.loggerTextbox.ensureCursorVisible()
+
+    def cancel(self):
+        if self.running > 0:
+            self.main_t.terminate()
+            self.main_t.wait()
+            self.running -= 1
+        self.logger.hide()
+        self.loggerTextbox.clear()
+
+    def quit(self, event):
+        sys.stdout = sys.__stdout__
+        if self.running > 0:
+            self.cancel()
+        self.options.close()
+        self.logger.close()
+        self.close()
+
+    def done(self):
+        self.set_tracklist('')
+        self.set_output('')
+        self.optionsOk.setEnabled(True)
+        self.logger.hide()
+        self.loggerTextbox.clear()
+    
+    def show_help(self):
+        x = self.options.x() - self.options.width() - 50
+        y = self.options.y() - self.options.height()
+        self.help.move(x, y)
+        self.help.show()
+        self.optionsHelp.setEnabled(False)
+
+    def hide_help(self, signal):
+        self.help.hide()
+        self.optionsHelp.setEnabled(True)
+
+    def preMain(self):
+        self.optionsOk.setEnabled(False)
+        self.loggerDone.setEnabled(False)
+
+    def postMain(self):
+        self.loggerDone.setEnabled(True)
+
+    def main(self):
+        self.fnSyaArgs.tracklist = self.optionsValue[self.tracklistLabel]
+        self.fnSyaArgs.format = self.optionsValue[self.formatLabel]
+        self.fnSyaArgs.quality = self.optionsValue[self.qualityLabel]
+        self.fnSyaArgs.keep = self.optionsValue[self.keepLabel]
+        self.fnSyaArgs.output = self.optionsValue[self.outputLabel]
+
+        self.main_t = SyaGuiThread(self.fnSya, self.fnSyaArgs)
+        self.main_t.started.connect(self.preMain)
+        self.main_t.finished.connect(self.postMain)
+
+        self.logger.setWindowTitle(self.fnSyaArgs.output)
+        self.logger.show()
+        self.running += 1
+        self.main_t.start()
+
+    # optionsValue
+    def _init_options_value(self):
+        self.tracklistLabel = 'Tracklist:'
+        self.formatLabel = 'Format:'
+        self.qualityLabel = 'Quality:'
+        self.keepLabel = 'Keep unsplit file'
+        self.outputLabel = 'Output:'
+        self.optionsValue = {
+            self.tracklistLabel: self.fnSyaArgs.tracklist,
+            self.formatLabel: self.fnSyaArgs.format,
+            self.qualityLabel: self.fnSyaArgs.quality,
+            self.keepLabel: self.fnSyaArgs.keep,
+            self.outputLabel: self.fnSyaArgs.output
+        }
+
+    # options
+    def _init_options(self):
+        self.options = qtwidg.QWidget()
+        self.optionsOk = qtwidg.QPushButton('OK')
+        self.optionsHelp = qtwidg.QPushButton('Help')
+
         layout = qtwidg.QGridLayout()
-        # textbox
-        logbox = qtwidg.QPlainTextEdit()
-        logbox.setReadOnly(True)
-        self._logbox = logbox
-        layout.addWidget(logbox, 1, 0, 1, 5)
-        # cancel
-        cancel_btn = qtwidg.QPushButton('Cancel')
-        cancel_btn.clicked.connect(self._cancel)
-        layout.addWidget(cancel_btn, 2, 0)
-        # warning
-        warning = qtwidg.QLabel('Be patient, this might take a while. Click "Done" when finished.')
-        layout.addWidget(warning, 2, 1, 1, 2)
-        # done
-        self._done_btn = qtwidg.QPushButton('Done')
-        self._done_btn.clicked.connect(sys.exit)
-        self._done_btn.setEnabled(False)
-        layout.addWidget(self._done_btn, 2, 4)
-
-        logs.setLayout(layout)
-        return logs
-
-    def _init_filepicker(self, widget, labelText, filepickerFn, default=None, icon='folder'):
-        layout = qtwidg.QHBoxLayout()
-        # label
-        label = qtwidg.QLabel(labelText, widget)
-        layout.addWidget(label)
-        # line edit
-        self._edits[labelText] = qtwidg.QLineEdit(widget)
-        if default != None:
-            self._edits[labelText].setText(default)
-        layout.addWidget(self._edits[labelText])
-        # filepicker btn
-        button_logo = qtgui.QIcon(resource_path('{}.png'.format(icon)))
-        button = qtwidg.QPushButton(button_logo, '', widget)
-        button.clicked.connect(filepickerFn)
-        layout.addWidget(button)
+        layout.addLayout(self._init_options_tracklist(), 0, 0, 1, 3)
+        layout.addLayout(self._init_options_format(), 1, 0)
+        layout.addLayout(self._init_options_quality(), 2, 0)
+        layout.addItem(qtwidg.QSpacerItem(int(self.options.width()/4), 0, qtwidg.QSizePolicy.Expanding, qtwidg.QSizePolicy.Expanding))
+        layout.addWidget(self._init_options_keep(), 1, 2, 2, 1)
+        layout.addLayout(self._init_options_output(), 3, 0, 1, 3)
+        layout.addWidget(self.optionsHelp, 4, 0)
+        layout.addWidget(self.optionsOk, 4, 2)
+        
+        self.options.setLayout(layout)
+        self.options.setWindowTitle('sya (split youtube audio)')
+        self.options.setWindowIcon(qtgui.QIcon(resource_path('sya.png')))
 
-        return layout
+        self.update_options_ok()
+        self.options.show()
 
-    def _init_combobox(self, widget, label, setFn, options, default):
-        layout = qtwidg.QHBoxLayout()
-        # label
-        label = qtwidg.QLabel(label, widget)
-        layout.addWidget(label)
-        # combobox
-        combo = qtwidg.QComboBox(widget)
-        for opt in options:
-            combo.addItem(opt)
-        if default in options:
-            combo.setCurrentIndex(options.index(default))
-        combo.activated[str].connect(setFn)
-        layout.addWidget(combo)
-
-        layout.setStretch(0, 2)
+    def _init_options_tracklist(self):
+        label = self.tracklistLabel
+        layout, self.optionsTracklist = sya_gui_filepicker(self.options, label, self.select_tracklist, self.set_tracklist, self.optionsValue[label], 'file')
         return layout
-    
-    def _filepicker_tracklist(self, signal):
-        file = qtwidg.QFileDialog.getOpenFileName(self._options,
-            'Select a tracklist', os.path.expanduser('~'), "Text file (*.txt)",
-            None, qtwidg.QFileDialog.DontUseNativeDialog)
-        if len(file) > 0:
-            self.args.tracklist = file[0]
-            self._edits[self._tracklistLabel].setText(self.args.tracklist)
-            if len(self._edits[self._outputLabel].text()) == 0:
-                self.args.output = os.path.splitext(self.args.tracklist)[0]
-                self._edits[self._outputLabel].setText(self.args.output)
-            self._check_ok()
-
-    def _filepicker_output(self, signal):
-        file = qtwidg.QFileDialog.getExistingDirectory(self._options,
-            'Select directory', os.path.expanduser('~'),
-            qtwidg.QFileDialog.DontUseNativeDialog)
-        if len(file) > 0:
-            self.args.output = file
-            self._edits[self._outputLabel].setText(file)
-        self._check_ok()
 
-    def _set_format(self, format):
-        self.args.format = format
+    def _init_options_format(self):
+        label = self.formatLabel
+        self.optionsFormat = sya_gui_combobox(self.options, label, self.availableFormats, self.optionsValue[label], self.set_format)
+        return self.optionsFormat
 
-    def _set_quality(self, quality):
-        self.args.quality = quality[0]
+    def _init_options_quality(self):
+        label = self.qualityLabel
+        self.optionsQuality = sya_gui_combobox(self.options, label, self.availableQualities, self.optionsValue[label], self.set_quality)
+        return self.optionsQuality
 
-    def _keep_toggle(self):
-        self.args.keep = not self.args.keep
+    def _init_options_output(self):
+        label = self.outputLabel
+        layout, self.optionsOutput = sya_gui_filepicker(self.options, label, self.select_output, self.set_output, self.optionsValue[label], 'folder')
+        return layout
 
-    def _check_ok(self):
-        if self.args.tracklist != None and self.args.output != None and \
-        os.path.exists(self.args.tracklist) and len(self.args.output) > 0:
-            self._ok_btn.setEnabled(True)
+    def _init_options_keep(self):
+        label = self.keepLabel
+        self.optionsKeep = qtwidg.QCheckBox(label, self.options)
+        if self.optionsValue[label]:
+            self.optionsKeep.setChecked(True)
+        self.optionsKeep.toggled.connect(self.toggle_keep)
+        return self.optionsKeep
+
+    # Options Callbacks
+    def select_tracklist(self):
+        dialog = qtwidg.QFileDialog()
+        dialog.setWindowIcon(qtgui.QIcon(resource_path('sya.png')))
+        file = dialog.getOpenFileName(self.options, 'Select a tracklist', os.path.expanduser('~'), "Text file (*.txt)", None, qtwidg.QFileDialog.DontUseNativeDialog)
+        if len(file) > 0:
+            self.set_tracklist(file[0])
+
+    def set_tracklist(self, text):
+        self.optionsValue[self.tracklistLabel] = text
+        self.optionsTracklist.setText(text)
+        self.set_output(os.path.splitext(text)[0])
+        self.update_options_ok()
+
+    def select_output(self):
+        dialog = qtwidg.QFileDialog()
+        dialog.setWindowIcon(qtgui.QIcon(resource_path('sya.png')))
+        file = dialog.getExistingDirectory(self.options, 'Select directory', os.path.expanduser('~'), qtwidg.QFileDialog.DontUseNativeDialog)
+        if len(file) > 0:
+            self.set_output(file[0])
+
+    def set_output(self, text):
+        self.optionsValue[self.outputLabel] = text
+        self.optionsOutput.setText(text)
+        self.update_options_ok()
+
+    def set_format(self, option):
+        if option not in self.availableFormats:
+            return
+        self.optionsValue[self.formatLabel] = option
+        self.update_options_ok()
+
+    def set_quality(self, option):
+        if option not in self.availableQualities:
+            return
+        self.optionsValue[self.qualityLabel] = option
+        self.update_options_ok()
+
+    def toggle_keep(self):
+        self.optionsValue[self.keepLabel] = not self.optionsValue[self.keepLabel]
+        self.update_options_ok()
+
+    def update_options_ok(self):
+        tracklist = self.optionsValue[self.tracklistLabel]
+        output = self.optionsValue[self.outputLabel]
+        if os.path.exists(tracklist) and len(output) > 0:
+            self.optionsOk.setEnabled(True)
         else:
-            self._ok_btn.setEnabled(False)
+            self.optionsOk.setEnabled(False)
+
+    # Help Widget
+    def _init_help(self):
+        self.help = qtwidg.QTextEdit()
+        with open(resource_path("HELP.md")) as f:
+            self.help.setMarkdown(f.read())
+        self.help.resize(500, 500)
+        self.help.setReadOnly(True)
+        return
+
+    # Logger Widget
+    def _init_logger(self):
+        layout = qtwidg.QGridLayout()
+        layout.addWidget(self._init_logger_textbox(), 1, 0, 1, 5)
+        layout.addWidget(self._init_logger_cancel(), 2, 0)
+        layout.addWidget(self._init_logger_warning(), 2, 1, 1, 2)
+        layout.addWidget(self._init_logger_done(), 2, 4)
 
-    def _ok(self):
-        self._logs.show()
-        self.start_main()
+        self.logger = qtwidg.QWidget()
+        self.logger.setLayout(layout)
+        self.logger.setWindowIcon(qtgui.QIcon(resource_path('sya.png')))
+        self.logger.resize(800, 400)
 
-    def log(self, msg):
-        cursor = self._logbox.textCursor()
-        cursor.insertText(msg)
-        self._logbox.setTextCursor(cursor)
-        self._logbox.ensureCursorVisible()
-
-    def start_main(self):
-        self.main_t = SyaGuiMain(self.fnSya, args=self.args)
-        self.check_t = SyaGuiMain(self._check_done)
-        self.main_t.start()
-        self.check_t.start()
+    def _init_logger_textbox(self):
+        self.loggerTextbox = qtwidg.QPlainTextEdit()
+        self.loggerTextbox.setReadOnly(True)
+        self.loggerTextbox.setLineWrapMode(qtwidg.QPlainTextEdit.NoWrap)
+        return self.loggerTextbox
 
-    def _check_done(self):
-        while self.main_t.isFinished() != True:
-            continue
-        self._ok_btn.setEnabled(True)
-        self._options.setEnabled(False)
+    def _init_logger_cancel(self):
+        self.loggerCancel = qtwidg.QPushButton('Cancel')
+        return self.loggerCancel
 
-    def _cancel(self):
-        self.main_t.exit()
-        self.check_t.exit()
-        shutil.rmtree(self.args.output)
-        del(self._logs)
+    @staticmethod
+    def _init_logger_warning():
+        return qtwidg.QLabel('This might take a while. You can click "Done" when it\'s finished.')
 
+    def _init_logger_done(self):
+        self.loggerDone = qtwidg.QPushButton('Done')
+        self.loggerDone.setEnabled(False)
+        return self.loggerDone
+
+
+# Main
 if __name__ == '__main__':
     app = qtwidg.QApplication(sys.argv)
-    gui = SyaGui(sya.sya, sya.parse_args())
-    
-    if sys.platform == 'win32':
-        gui.args.youtubedl = resource_path('yt-dlp.exe')
-        gui.args.ffmpeg = resource_path('ffmpeg.exe')
-    else:
-        gui.args.youtubedl = resource_path('yt-dlp')
-        gui.args.ffmpeg = resource_path('ffmpeg')
-    
-    sys.exit(app.exec_())
 
+    args = sya.parse_args()
+    if args.tracklist is None:
+        args.tracklist = ''
+    if args.output is None:
+        args.output = ''
+    if args.youtubedl is None:
+        args.youtubedl = resource_path('yt-dlp') if sys.platform != 'win32' else resource_path('yt-dlp.exe')
+    if args.ffmpeg is None:
+        args.ffmpeg = resource_path('ffmpeg') if sys.platform != 'win32' else resource_path('ffmpeg.exe')
+    gui = SyaGui(sya.sya, args)
+
+    sys.exit(app.exec_())
diff --git a/sya.png b/sya.png
Binary files differ.
diff --git a/sya.py b/sya.py
@@ -15,34 +15,30 @@ class TracklistItem:
         self.title = title
 
 # utilities
-def log(msg):
-    print(msg)
-
 def error_exit(msg):
-    log('exit failure "{}"'.format(msg))
+    print('exit failure "{}"'.format(msg))
     sys.exit()
 
 def check_bin(*binaries):
     for b in binaries:
         try:
-            subprocess.call([b], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
+            subprocess.call([b], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, shell=False)
         except:
             error_exit('failed to execute {}'.format(b))
 
 # functions
 def get_audio(youtubedl, url, outdir, format='mp3', quality='320K', keep=True, ffmpeg='ffmpeg'):
-    log('{} getting {}, {} ({})'.format(youtubedl, format, quality, url))
+    print('Downloading {} ({}, {})...'.format(url, format, quality))
     fname = '{}/{}'.format(outdir, os.path.basename(outdir), format)
-    cmd = [youtubedl, url, '--newline', '--extract-audio', '--audio-format', format,
-        '--audio-quality', quality, '--prefer-ffmpeg', '-o', fname + '.%(ext)s']
+    cmd = [youtubedl, '--newline', '--extract-audio', '--audio-format', format,
+        '--audio-quality', quality, '--prefer-ffmpeg', '--ffmpeg-location', ffmpeg,
+        '-o', fname + '.%(ext)s']
     if keep == True:
         cmd.append('-k')
-    if ffmpeg != 'ffmpeg':
-        cmd.append('--ffmpeg-location')
-        cmd.append(ffmpeg)
-    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-    for line in iter(p.stdout.readline, b''):
-        log(line.decode('utf-8').rstrip('\n'))
+    cmd.append(url)
+    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
+    for line in p.stdout.readlines():
+        print('    {}'.format(line.decode('utf-8', errors='ignore').strip()))
     return '{}.{}'.format(fname, format)
 
 def load_tracklist(path):
@@ -57,16 +53,25 @@ def load_tracklist(path):
 
 def parse_tracks(tracklist):
     tracks = []
+    weightR = 0 # num. timestamps on right-side
+    weightL = 0 # num. timestamps on left-side
     for lcount, line in enumerate(tracklist):
         sline = line.split(' ')
         
         timestamp = None
-        for l in sline:
-            if Timestamp.match(l):
-                timestamp = l.strip('[()]')
+        for i, l in enumerate(sline):
+            if i != 0 and i != len(sline)-1:
+                continue
+            elif Timestamp.match(l):
+                if timestamp == None or weightR > weightL:
+                    timestamp = l.strip('[()]')
+                if i == 0:
+                    weightL += 1
+                else:
+                    weightR += 1
                 sline.remove(l)
         if timestamp == None:
-            log('line {}, missing timestamp: "{}"'.format(lcount, line))
+            print('line {}, missing timestamp: "{}"'.format(lcount, line))
         
         title = ' '.join(sline).strip(' ')
         title = re.sub(r"[/\\?%*:|\"<>\x7F\x00-\x1F]", '', title)
@@ -81,30 +86,36 @@ def missing_times(tracks):
             missing.append(i)
     return missing
 
-def split_tracks(ffmpeg, audio_fpath, tracks, format='mp3', outpath='out'):
-    log('splitting tracks...')
-    cmd = ['ffmpeg', '-v', 'quiet', '-stats', '-i', audio_fpath, '-f', 'null', '-']
-    ret = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
-    # some nasty string manip. to extract length (printed to stderr)
+def read_tracklen(ffmpeg, track_fpath):
+    cmd = [ffmpeg, '-v', 'quiet', '-stats', '-i', track_fpath, '-f', 'null', '-']
+    length = '00:00'
     try:
+        ret = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=False)
         length = str(ret).split('\\r')
-        length = length[len(length)-1].split(' ')[1].split('=')[1][:-3]
+        # some nasty string manip. to extract length (printed to stderr)
+        if sys.platform == 'win32':
+            length = length[len(length)-2].split(' ')[1].split('=')[1][:-3]
+        else:
+            length = length[len(length)-1].split(' ')[1].split('=')[1][:-3]
+        print('Track length: {}'.format(length))
     except:
-        log('Failed to find track length, {}'.format(length))
-        return
-    
+        error_exit('Failed to find track length, aborting.')
+    return length
+
+def split_tracks(ffmpeg, audio_fpath, audio_len, tracks, format='mp3', outpath='out'):    
+    print('Splitting...')
     for i, t in enumerate(tracks):
         outfile = '{}/{} - {}.{}'.format(outpath, str(i+1).zfill(2), t.title.strip(' - '), format)
-        end = length
+        end = audio_len
         if i < len(tracks)-1:
             end = tracks[i+1].timestamp
-        log('\t{} ({} - {})'.format(outfile, t.timestamp, end))
+        print('     {} ({} - {})'.format(outfile, t.timestamp, end))
         cmd = ['ffmpeg', '-nostdin', '-y', '-loglevel', 'error', 
             '-i', audio_fpath, '-ss', t.timestamp, '-to', end,
             '-acodec', 'copy', outfile]
-        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-        for line in iter(p.stdout.readline, b''):
-            log(line.decode('utf-8').rstrip('\n'))
+        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
+        for line in p.stdout.readlines():
+            print('    {}'.format(line.decode('utf-8', errors='ignore').strip()))
     return
 
 # runtime
@@ -115,21 +126,32 @@ def parse_args():
     parser.add_argument('tracklist', metavar='TRACKLIST', nargs='?',
         help='tracklist to split audio by')
     # options
-    parser.add_argument('-o', '--output', metavar='PATH', type=str, nargs='?', dest='output',
+    parser.add_argument('-o', '--output',
+        metavar='PATH', type=str, nargs='?', dest='output',
         help='specify the directory to write output files to (default: ./out)')
-    parser.add_argument('-f', '--format', type=str, nargs='?', default='mp3', dest='format',
+    parser.add_argument('-f', '--format',
+        type=str, nargs='?', default='mp3', dest='format',
         help='specify the --audio-format argument to pass to yt-dlp (default: mp3)')
-    parser.add_argument('-q', '--quality', type=str, nargs='?', default='320K', dest='quality',
+    parser.add_argument('-q', '--quality',
+        type=str, nargs='?', default='320K', dest='quality',
         help='specify the --audio-quality argument to pass to yt-dlp (default: 320K)')
-    parser.add_argument('--yt-dlp', metavar='PATH', type=str, nargs='?', default='yt-dlp', dest='youtubedl',
+    parser.add_argument('--yt-dlp',
+        metavar='PATH', type=str, nargs='?', dest='youtubedl',
         help='path of the "yt-dlp" binary to use')
-    parser.add_argument('--ffmpeg', metavar='PATH', type=str, nargs='?', default='ffmpeg', dest='ffmpeg',
+    parser.add_argument('--ffmpeg',
+        metavar='PATH', type=str, nargs='?', dest='ffmpeg',
         help='path of the "ffmpeg" binary to use')
-    parser.add_argument('-k', '--keep', action='store_true', default=False, dest='keep',
+    parser.add_argument('-k', '--keep',
+        action='store_true', default=False, dest='keep',
         help='keep any files removed during processing (full video/audio file)')
     return parser.parse_args()
 
 def sya(args):
+    if args.youtubedl == None:
+        args.youtubedl = 'yt-dlp.exe' if sys.platform == 'win32' else 'yt-dlp'
+    if args.ffmpeg == None:
+        args.ffmpeg = 'ffmpeg.exe' if sys.platform == 'win32' else 'ffmpeg'
+
     if check_bin(args.youtubedl, args.ffmpeg) == False:
         error_exit('required binaries are missing')
     if args.tracklist == None or os.path.exists(args.tracklist) == False:
@@ -141,18 +163,24 @@ def sya(args):
     
     audio_fpath = get_audio(args.youtubedl, tracklist[0], args.output,
             args.format, args.quality, args.keep, args.ffmpeg)
-            
-    tracks = parse_tracks(tracklist[1:])
+    if os.path.exists(audio_fpath) == False:
+        error_exit('download failed, aborting')
 
+    
+    tracks = parse_tracks(tracklist[1:])
+    
     missing = missing_times(tracks)
     if len(missing) > 0:
         error_exit('some tracks are missing timestamps')
 
+    length = read_tracklen(args.ffmpeg, audio_fpath)
     os.makedirs(args.output, exist_ok=True)
-    split_tracks(args.ffmpeg, audio_fpath, tracks, args.format, args.output)
+    split_tracks(args.ffmpeg, audio_fpath, length, tracks, args.format, args.output)
 
     if args.keep is False:
         os.remove(audio_fpath)
 
+    print('Success')
+
 if __name__ == '__main__':
     sya(parse_args())