├── .gitignore ├── README.md ├── _config.yml ├── application.py ├── audiojack_gui.py ├── audiojackgui.kv ├── icon.ico ├── img └── loading.png ├── launcher.py ├── logo └── logo.png ├── screenshots ├── scr_0.png ├── scr_1.png ├── scr_2.png └── scr_3.png └── settings.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.pyc 3 | *.mp3 4 | *.exe 5 | /audiojack.py 6 | /audiojackgui.ini 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![AudioJack-GUI](/logo/logo.png) 2 | 3 | A smart YouTube-to-MP3 converter that automatically finds and adds ID3 tags (artist, title, album, cover art) to downloaded MP3 files. 4 | 5 | ## Disclaimer 6 | This program is strictly intended for demonstration purposes. Using this program to download online media may breach the corresponding website's terms of service and may even be illegal in your country. Use this program at your own discretion. 7 | 8 | ## Screenshots 9 | ![AudioJack-GUI in action](/screenshots/scr_2.png) 10 | 11 | ## Step-by-step guide 12 | This short guide will show you how to get the source version of the program up and running. 13 | 14 | 1. Install Python 3.6+. 15 | 2. Install Kivy by following the guide [here](https://kivy.org/docs/installation/installation.html#stable-version). 16 | 3. Open Command Prompt (or Terminal, depending on your OS). 17 | 4. Type in the following command: `pip install mutagen musicbrainzngs youtube_dl pyperclip validators Pillow`. 18 | 5. Download [AudioJack-GUI](https://github.com/Blue9/AudioJack-GUI/archive/master.zip) extract the files. 19 | 6. Download [`audiojack.py`](https://github.com/Blue9/AudioJack/blob/master/audiojack.py) and place it in the `AudioJack-GUI` folder. 20 | 7. Download [FFmpeg](https://ffmpeg.org/download.html) and place the files `ffmpeg`, `ffprobe`, and `ffplay` in the same folder. 21 | 8. Navigate Command Prompt to the folder and run `python launcher.py`. 22 | 23 | Whenever you want to run the program, just repeat step 8. 24 | 25 | ## Usage 26 | After installing necessary requirements, using the program is quite self-explanatory. 27 | 28 | 0. Run `python launcher.py`. 29 | ![Step 0](/screenshots/scr_0.png) 30 | 1. Press F1 to change the settings such as the download path for the MP3s. (optional) 31 | ![Step 1](/screenshots/scr_1.png) 32 | 2. Enter a YouTube or SoundCloud URL in the input box and wait for the results to load. 33 | ![Step 2](/screenshots/scr_2.png) 34 | 3. Select the tags that correspond to your song. 35 | ![Step 3](/screenshots/scr_2.png) 36 | 4. Voilà! The MP3 file is now downloaded. You can trim the file if you wish (this is useful if you are converting a music video). 37 | ![Step 4](/screenshots/scr_3.png) 38 | 39 | ## Requirements 40 | 1. Python 3.6+ (**not Python 2**) 41 | 2. [AudioJack](https://github.com/Blue9/AudioJack) 42 | 3. [FFmpeg](https://www.ffmpeg.org/) (for MP3 conversion). 43 | 4. In addition, you will need to install the following modules for AudioJack to work: 44 | - [mutagen](https://bitbucket.org/lazka/mutagen) 45 | - [musicbrainzngs v0.6](https://github.com/alastair/python-musicbrainzngs) 46 | - [youtube-dl](https://github.com/rg3/youtube-dl) 47 | - [pyperclip](https://github.com/asweigart/pyperclip) 48 | - [validators](https://validators.readthedocs.io/en/latest/) 49 | - [Pillow](https://pillow.readthedocs.io) 50 | - [Kivy](https://kivy.org/doc/stable/gettingstarted/installation.html) 51 | 52 | ## Contribution: 53 | - Contributing to this project is highly encouraged. 54 | 55 | ### Guidelines: 56 | 1. Use single-quotes for string literals. 57 | 2. If you use any additional modules, please update the [requirements](#requirements) if necessary. 58 | 3. Thoroughly test the program before pushing. 59 | 60 | ## Credits: 61 | - Hoverable button functionality: [link](https://gist.github.com/opqopq/15c707dc4cffc2b6455f) (modified) 62 | - Loading album art image: [link](https://commons.wikimedia.org/wiki/File:No-album-art.png) 63 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /application.py: -------------------------------------------------------------------------------- 1 | from threading import RLock 2 | from audiojack import AudioJack 3 | 4 | 5 | class AudioJackApplication(object): 6 | def __init__(self): 7 | self.lock = RLock() 8 | self.observers = [] 9 | self.requests = [] # Used to record all previous searches, could be used for some features 10 | 11 | def add_observer(self, observer): 12 | self.observers.append(observer) 13 | 14 | @property 15 | def last_search(self): 16 | try: 17 | self.lock.acquire() 18 | if len(self.requests) > 0: 19 | return self.requests[-1] 20 | else: 21 | return None 22 | finally: 23 | self.lock.release() 24 | 25 | def search(self, url): 26 | search = SearchRequest(url) 27 | try: 28 | self.lock.acquire() 29 | self.requests.append(search) 30 | finally: 31 | self.lock.release() 32 | self.notify() 33 | 34 | def select(self, index): 35 | try: 36 | self.lock.acquire() 37 | self.last_search.select(index) 38 | finally: 39 | self.lock.release() 40 | self.notify() 41 | 42 | def notify(self): 43 | for observer in self.observers: 44 | observer.notify() 45 | 46 | 47 | class SearchRequest(object): 48 | def __init__(self, url=None): 49 | self.error = 0 50 | self.results = None 51 | self.selection = None 52 | self.file = None 53 | self.audiojack = AudioJack(small_cover_art=True) 54 | self.url = url 55 | if url: 56 | self.search(url) 57 | 58 | def search(self, url): 59 | try: 60 | self.results = self.audiojack.get_results(url) 61 | except AttributeError as e: 62 | print(e) 63 | self.error = 1 64 | 65 | def select(self, index, path=None): 66 | self.selection = self.results[index] 67 | try: 68 | self.file = self.audiojack.select(self.selection, path=path) 69 | except AttributeError as e: 70 | print(e) 71 | self.error = 1 72 | 73 | def custom(self, title, artist, album, path=None): 74 | try: 75 | self.file = self.audiojack.select({ 76 | 'url': self.url, 77 | 'title': title, 78 | 'artist': artist, 79 | 'album': album 80 | }, path=path) 81 | except AttributeError as e: 82 | print(e) 83 | self.error = 1 84 | 85 | def cut_file(self, start, end): 86 | self.audiojack.cut_file(self.file, start, end) 87 | -------------------------------------------------------------------------------- /audiojack_gui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import webbrowser 3 | import pyperclip 4 | from functools import partial 5 | from threading import Thread, current_thread 6 | from kivy.app import App 7 | from kivy.clock import Clock 8 | from kivy.loader import Loader 9 | from kivy.modules import inspector 10 | from kivy.core.window import Window 11 | from kivy.properties import BooleanProperty, NumericProperty 12 | from kivy.uix.widget import Widget 13 | from kivy.uix.button import Button, ButtonBehavior 14 | from kivy.uix.floatlayout import FloatLayout 15 | from kivy.uix.relativelayout import RelativeLayout 16 | from kivy.uix.image import AsyncImage 17 | from kivy.uix.label import Label 18 | from kivy.uix.settings import Settings, SettingPath 19 | 20 | Loader.num_workers = 8 21 | Loader.max_upload_per_frame = 8 22 | Loader.loading_image = 'img/loading.png' 23 | 24 | 25 | class SettingDirectory(SettingPath): 26 | def _validate(self, instance): 27 | super(SettingDirectory, self)._validate(instance) 28 | if not os.path.isdir(self.value): 29 | self.value = os.path.dirname(self.value) 30 | 31 | 32 | class MainGUI(FloatLayout): 33 | def __init__(self): 34 | super(MainGUI, self).__init__() 35 | 36 | 37 | class Hoverable(Widget): 38 | hover = BooleanProperty(False) 39 | 40 | def __init__(self, **kwargs): 41 | Window.bind(mouse_pos=self.on_mouse_pos) 42 | super(Hoverable, self).__init__(**kwargs) 43 | 44 | def on_mouse_pos(self, *args): 45 | if self.parent: 46 | pos = args[1] 47 | # We use self.parent.to_widget rather than self.to_widget because the result buttons' positions are 48 | # relative to their parents (this is not a issue for the other hoverable buttons). 49 | hovered = self.collide_point(*self.parent.to_widget(*pos)) 50 | if self.hover != hovered: 51 | self.hover = hovered 52 | 53 | 54 | class HoverableButton(Button, Hoverable): 55 | pass 56 | 57 | 58 | class CoverArt(AsyncImage): 59 | overlay = NumericProperty(0) 60 | 61 | def __init__(self, **kwargs): 62 | super(CoverArt, self).__init__(**kwargs) 63 | self.anim_delay = 0.1 64 | 65 | 66 | class ResultButton(ButtonBehavior, RelativeLayout, Hoverable): 67 | def __init__(self, result): 68 | super(ResultButton, self).__init__() 69 | self.label = Label(text='%s\n%s\n%s' % (result['title'], result['artist'], result['album']), halign='center', 70 | font_size='20sp', size_hint=(1, 1), opacity=0) 71 | self.image = CoverArt(source=result['img'], allow_stretch=True, size_hint=(1, 1)) 72 | self.add_widget(self.image) 73 | self.add_widget(self.label) 74 | self.size_hint = (0.25, None) 75 | self.bind(width=self.set_height) 76 | self.bind(hover=self.on_hover) 77 | 78 | def set_height(self, *args): 79 | self.height = self.width 80 | self.label.text_size = (self.width, None) 81 | 82 | def on_hover(self, *args): 83 | if self.hover: 84 | self.label.opacity = 1 85 | self.image.overlay = 0.6 86 | else: 87 | self.label.opacity = 0 88 | self.image.overlay = 0 89 | 90 | 91 | class AudioJackGUI(App): 92 | use_kivy_settings = False 93 | 94 | def __init__(self): 95 | super(AudioJackGUI, self).__init__() 96 | self.audiojack = None 97 | self.gui = None 98 | self.local_search = None 99 | self._keyboard = None 100 | self.current_search_thread = None 101 | 102 | self.error_msg = None 103 | self.loading_msg = None 104 | self.results = None 105 | self.open_file_button = None 106 | self.cut_file = None 107 | self.path = None 108 | 109 | def build(self): 110 | self.gui = MainGUI() 111 | self.initialize_widgets() 112 | Clock.schedule_interval(self.check_cb, 1) 113 | inspector.create_inspector(Window, self.gui) 114 | return self.gui 115 | 116 | def build_config(self, config): 117 | config.setdefaults('main', { 118 | 'dl_path': os.getcwd(), 119 | 'auto_cb': 1 120 | }) 121 | self.path = config.get('main', 'dl_path') 122 | 123 | def build_settings(self, settings): 124 | settings.register_type('dir', SettingDirectory) 125 | settings.add_json_panel('Main', self.config, 'settings.json') 126 | 127 | def on_config_change(self, config, section, key, value): 128 | self.path = config.get('main', 'dl_path') 129 | 130 | def check_cb(self, dt): 131 | if self.config.getboolean('main', 'auto_cb'): 132 | cb = pyperclip.paste() 133 | # Lazy url validation 134 | if cb.startswith('http'): 135 | # This is necessary to reset the cursor position in the TextInput box. 136 | self.gui.ids.url_input.text = '' 137 | self.gui.ids.url_input.text = cb 138 | 139 | def initialize_widgets(self): 140 | self.error_msg = self.gui.ids.error_msg.__self__ # Keep a reference to prevent GC when removing from the GUI. 141 | self.loading_msg = self.gui.ids.loading_msg.__self__ 142 | self.results = self.gui.ids.results.__self__ 143 | self.open_file_button = self.gui.ids.open_file_button.__self__ 144 | self.cut_file = self.gui.ids.cut_file.__self__ 145 | self.hide_error() 146 | self.hide_loading() 147 | self.hide_all() 148 | self.gui.ids.btn_submit.bind(on_release=self.search) 149 | self.gui.ids.submit_custom.bind(on_release=self.custom) 150 | self.gui.ids.url_input.bind(on_text_validate=self.search, focus=self.auto_hide_error) 151 | self.gui.ids.results_grid.bind(minimum_height=self.gui.ids.results_grid.setter('height')) 152 | 153 | def search(self, *args): 154 | self.hide_error() 155 | self.hide_all() 156 | url = self.gui.ids.url_input.text 157 | self.loading() 158 | # call self._search in new thread 159 | self.current_search_thread = Thread(target=self._search, args=(url,)) 160 | self.current_search_thread.start() 161 | 162 | def _search(self, url): 163 | self.audiojack.search(url) 164 | 165 | def select(self, index, *args): 166 | print(self.path) 167 | self.hide_all() 168 | self.hide_error() 169 | self.loading() 170 | self.current_search_thread = Thread(target=self._select, args=(index,)) 171 | self.current_search_thread.start() 172 | return True 173 | 174 | def _select(self, index): 175 | self.local_search.select(index, path=self.path) 176 | self.notify() 177 | 178 | def custom(self, *args): 179 | self.hide_all() 180 | self.hide_error() 181 | self.loading() 182 | self.current_search_thread = Thread(target=self._custom) 183 | self.current_search_thread.start() 184 | 185 | def _custom(self): 186 | self.local_search.custom(self.gui.ids.custom_title.text, self.gui.ids.custom_artist.text, 187 | self.gui.ids.custom_album.text, path=self.path) 188 | self.notify() 189 | 190 | def hide_all(self): 191 | self.hide_results() 192 | self.hide_file() 193 | self.hide_cut_file() 194 | 195 | def hide_results(self, *args): 196 | if self.results.parent: 197 | self.results.parent.remove_widget(self.results) 198 | self.gui.ids.results_grid.clear_widgets() 199 | 200 | def handle_results(self, *args): 201 | if len(self.local_search.results) == 0: 202 | self.gui.ids.results_label.text = 'No results found.' 203 | else: 204 | self.gui.ids.results_label.text = '%d results found.' % len(self.local_search.results) 205 | for i, result in enumerate(self.local_search.results): 206 | result_btn = ResultButton(result) 207 | result_btn.bind(on_release=partial(self.select, i)) 208 | self.gui.ids.results_grid.add_widget(result_btn) 209 | self.show_results() 210 | 211 | def show_results(self): 212 | if not self.results.parent: 213 | self.gui.ids.main_layout.add_widget(self.results) 214 | 215 | def handle_selection(self, *args): 216 | print(self.local_search.selection) 217 | 218 | def hide_file(self): 219 | if self.open_file_button.parent: 220 | self.open_file_button.parent.remove_widget(self.open_file_button) 221 | 222 | def hide_cut_file(self): 223 | if self.cut_file.parent: 224 | self.cut_file.parent.remove_widget(self.cut_file) 225 | 226 | def handle_file(self, *args): 227 | self.open_file_button.text = 'Open %s' % self.local_search.file 228 | self.open_file_button.bind(on_release=self.open_file) 229 | self.gui.ids.cut_file_btn.bind(on_release=self.cut) 230 | if not self.open_file_button.parent: 231 | self.gui.ids.main_layout.add_widget(self.open_file_button) 232 | if not self.cut_file.parent: 233 | self.gui.ids.main_layout.add_widget(self.cut_file) 234 | 235 | def open_file(self, *args): 236 | webbrowser.open(self.local_search.file) 237 | 238 | def cut(self, *args): 239 | start = self.gui.ids.start_time.text 240 | start = int(start) if start.isdigit() else 0 241 | end = self.gui.ids.end_time.text 242 | end = int(end) if end.isdigit() else None 243 | self.local_search.cut_file(start, end) 244 | 245 | def hide_loading(self, *args): 246 | if self.loading_msg.parent: 247 | self.loading_msg.parent.remove_widget(self.loading_msg) 248 | 249 | def loading(self): 250 | if not self.loading_msg.parent: 251 | self.gui.ids.main_layout.add_widget(self.loading_msg) 252 | 253 | def hide_error(self, *args): 254 | if self.error_msg.parent: 255 | self.error_msg.parent.remove_widget(self.error_msg) 256 | 257 | def auto_hide_error(self, *args): 258 | if self.gui.ids.url_input.focus: 259 | self.hide_error() 260 | 261 | def handle_error(self, error, *args): 262 | # TODO: More descriptive error messages 263 | self.hide_all() 264 | if not self.error_msg.parent: 265 | self.gui.ids.main_layout.add_widget(self.error_msg) 266 | self.error_msg.text = 'An unknown error occurred.' 267 | 268 | def notify(self): 269 | if current_thread() == self.current_search_thread: 270 | print('Notified current thread') 271 | self.hide_loading() 272 | self.local_search = self.audiojack.last_search 273 | if self.local_search: 274 | if self.local_search.error != 0: 275 | # GUI changes must be done in the main thread. 276 | Clock.schedule_once(partial(self.handle_error, self.local_search.error)) 277 | else: 278 | if self.local_search.file: 279 | Clock.schedule_once(self.handle_file) 280 | elif self.local_search.selection: 281 | Clock.schedule_once(self.handle_selection) 282 | elif self.local_search.results is not None: 283 | # An empty array by itself will evaluate to false so we check if it equals None instead. 284 | Clock.schedule_once(self.handle_results) 285 | else: 286 | print('Notified outdated thread') 287 | -------------------------------------------------------------------------------- /audiojackgui.kv: -------------------------------------------------------------------------------- 1 | #:kivy 1.9.1 2 | 3 | : 4 | color_theme: (0.07, 0.21, 0.42, 1.0) 5 | canvas.before: 6 | Color: 7 | rgba: root.color_theme 8 | Rectangle: 9 | size: self.size 10 | pos: self.pos 11 | 12 | StackLayout: 13 | id: main_layout 14 | orientation: 'tb-lr' 15 | size_hint: (0.8, 1.0) 16 | pos_hint: {'center_x': 0.5} 17 | spacing: [0, 10] 18 | 19 | Label: 20 | # Logo 21 | text: 'AudioJack' 22 | font_size: '64sp' 23 | size_hint: (1.0, 0.2) 24 | 25 | BoxLayout: 26 | size_hint: (1.0, 0.1) 27 | TextInput: 28 | # URL input 29 | id: url_input 30 | background_normal: '' 31 | background_active: '' 32 | hint_text: 'Enter a YouTube or SoundCloud URL' 33 | font_size: '40sp' 34 | size_hint: (0.9, 1.0) 35 | multiline: False 36 | padding: self.padding[0], (self.height - self.line_height) / 2 37 | 38 | HoverableButton: 39 | # Search button 40 | id: btn_submit 41 | size_hint: (0.1, 1.0) 42 | text: 'Search' 43 | # background_down: '' 44 | background_normal: '' 45 | 46 | Label: 47 | # Error messages will be presented through this label 48 | id: error_msg 49 | color: (1, 0, 0, 1) 50 | size_hint: (1.0, 0.08) 51 | font_size: '20sp' 52 | text: 'Error placeholder' 53 | 54 | Label: 55 | id: loading_msg 56 | color: (1, 1, 1, 1) 57 | size_hint: (1.0, 0.08) 58 | font_size: '20sp' 59 | text: 'Loading...' 60 | 61 | HoverableButton: 62 | # Button to open downloaded file 63 | id: open_file_button 64 | size_hint: (1.0, 0.08) 65 | font_size: '20sp' 66 | # background_down: '' 67 | background_normal: '' 68 | 69 | BoxLayout: 70 | # Contains options to cut file 71 | orientation: 'vertical' 72 | id: cut_file 73 | size_hint: (1.0, 0.3) 74 | spacing: 5 75 | pos_hint: {'center_x': 0.5} 76 | 77 | Label: 78 | text: 'Cut file' 79 | font_size: '20sp' 80 | color: (1, 1, 1, 1) 81 | size_hint: (0.4, 0.3) 82 | pos_hint: {'center_x': 0.5} 83 | 84 | TextInput: 85 | id: start_time 86 | size_hint: (0.4, 0.25) 87 | multiline: False 88 | background_normal: '' 89 | hint_text: 'Start time (s)' 90 | font_size: '20sp' 91 | padding: self.padding[0], (self.height - self.line_height) / 2 92 | pos_hint: {'center_x': 0.5} 93 | 94 | TextInput: 95 | id: end_time 96 | size_hint: (0.4, 0.25) 97 | multiline: False 98 | background_normal: '' 99 | hint_text: 'End time (s)' 100 | font_size: '20sp' 101 | padding: self.padding[0], (self.height - self.line_height) / 2 102 | pos_hint: {'center_x': 0.5} 103 | 104 | HoverableButton: 105 | text: 'Cut' 106 | id: cut_file_btn 107 | size_hint: (0.4, 0.2) 108 | pos_hint: {'center_x': 0.5} 109 | 110 | BoxLayout: 111 | # Results 112 | orientation: 'vertical' 113 | id: results 114 | size_hint: (1.0, 0.54) 115 | 116 | Label: 117 | id: results_label 118 | text: 'Result information placeholder' 119 | color: (1, 1, 1, 1) 120 | font_size: '20sp' 121 | size_hint: (1.0, 0.1) 122 | 123 | ScrollView: 124 | size_hint: (1.0, 0.9) 125 | 126 | GridLayout: 127 | cols: 1 128 | size_hint_y: None 129 | height: self.minimum_height 130 | 131 | StackLayout: 132 | # Results grid 133 | orientation: 'lr-tb' 134 | id: results_grid 135 | size_hint_y: None 136 | spacing: 10 137 | 138 | BoxLayout: 139 | # Custom selection 140 | orientation: 'vertical' 141 | id: custom_selection 142 | size_hint_y: None 143 | spacing: 4 144 | height: '200px' 145 | 146 | Label: 147 | text: 'Custom selection' 148 | font_size: '20sp' 149 | size_hint_x: 0.5 150 | pos_hint: {'center_x': 0.5} 151 | 152 | TextInput: 153 | hint_text: 'Title' 154 | id: custom_title 155 | size_hint_x: 0.5 156 | pos_hint: {'center_x': 0.5} 157 | padding: self.padding[0], (self.height - self.line_height) / 2 158 | 159 | TextInput: 160 | hint_text: 'Artist' 161 | id: custom_artist 162 | size_hint_x: 0.5 163 | pos_hint: {'center_x': 0.5} 164 | padding: self.padding[0], (self.height - self.line_height) / 2 165 | 166 | TextInput: 167 | hint_text: 'Album' 168 | id: custom_album 169 | size_hint_x: 0.5 170 | pos_hint: {'center_x': 0.5} 171 | padding: self.padding[0], (self.height - self.line_height) / 2 172 | 173 | HoverableButton: 174 | text: 'Go!' 175 | id: submit_custom 176 | size_hint_x: 0.5 177 | pos_hint: {'center_x': 0.5} 178 | 179 | : 180 | canvas.after: 181 | Color: 182 | rgba: (0, 0, 0, self.overlay) 183 | Rectangle: 184 | size: self.size 185 | pos: self.pos 186 | 187 | : 188 | color: (0.07, 0.21, 0.42, 1.0) 189 | background_color: (0.3, 0.5, 0.75, 1) if self.hover else (0.8, 0.9, 0.95, 1) 190 | background_normal: '' 191 | -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/AudioJack-GUI/e5dbd475a84213978c0899ca709a315423f260e1/icon.ico -------------------------------------------------------------------------------- /img/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/AudioJack-GUI/e5dbd475a84213978c0899ca709a315423f260e1/img/loading.png -------------------------------------------------------------------------------- /launcher.py: -------------------------------------------------------------------------------- 1 | from application import AudioJackApplication 2 | from audiojack_gui import AudioJackGUI 3 | 4 | 5 | def launch(): 6 | application = AudioJackApplication() 7 | gui = AudioJackGUI() 8 | 9 | application.add_observer(gui) # GUI is notified when application changes 10 | gui.audiojack = application 11 | 12 | gui.run() 13 | 14 | 15 | if __name__ == '__main__': 16 | launch() 17 | -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/AudioJack-GUI/e5dbd475a84213978c0899ca709a315423f260e1/logo/logo.png -------------------------------------------------------------------------------- /screenshots/scr_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/AudioJack-GUI/e5dbd475a84213978c0899ca709a315423f260e1/screenshots/scr_0.png -------------------------------------------------------------------------------- /screenshots/scr_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/AudioJack-GUI/e5dbd475a84213978c0899ca709a315423f260e1/screenshots/scr_1.png -------------------------------------------------------------------------------- /screenshots/scr_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/AudioJack-GUI/e5dbd475a84213978c0899ca709a315423f260e1/screenshots/scr_2.png -------------------------------------------------------------------------------- /screenshots/scr_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue9/AudioJack-GUI/e5dbd475a84213978c0899ca709a315423f260e1/screenshots/scr_3.png -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "dir", 4 | "title": "Download path", 5 | "desc": "Files will be downloaded here", 6 | "key": "dl_path", 7 | "section": "main" 8 | }, 9 | { 10 | "type": "bool", 11 | "title": "Auto clipboard paste", 12 | "desc": "If true, the URL input will automatically be filled with the clipboard content", 13 | "key": "auto_cb", 14 | "section": "main" 15 | } 16 | ] --------------------------------------------------------------------------------