├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature-request-suggestion.md └── donation.md ├── .gitignore ├── Docs.md ├── LICENSE ├── README.md ├── images ├── all_stages.png └── donation_qr.png └── src ├── SW-DLT.plist └── SW_DLT.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: ['https://github.com/net00-1/SW-DLT/blob/master/.github/donation.md'] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Describe an issue you encountered 4 | title: "[Issue]" 5 | labels: Bug 6 | assignees: net00-1 7 | 8 | --- 9 | 10 | **Describe the Issue you encountered:** 11 | 12 | - 13 | 14 | **Provide Essential Details for troubleshooting** 15 | - iOS/iPadOS version: 16 | - Device version: 17 | - URL: 18 | - Options used: 19 | 20 | **(Mandatory) Add Screenshot of Error popup from Shortcut** 21 | 22 | - 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request-suggestion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request/Suggestion 3 | about: Provide details of a feature you would like implemented 4 | title: "[Suggestion]" 5 | labels: Suggestion 6 | assignees: net00-1 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/donation.md: -------------------------------------------------------------------------------- 1 | # Donations 2 | **If this shortcut has helped you, please consider leaving a small tip by scanning the below QR code. All tips are very much appreciated!** 3 | 4 |
5 | 6 | **[Donate with BCH](https://deep.edge.app/pay/bitcoincash/qq5l9j3cgah8s6rz3xd5aet0u8wml2ezsgst9fedzz?amount=0.0086369)** 7 | 8 | 9 | 10 |
11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /util/staticLinkRemoval.txt 2 | /tests/private_tests.py 3 | /swdlt_env/ 4 | launch.json 5 | -------------------------------------------------------------------------------- /Docs.md: -------------------------------------------------------------------------------- 1 | # SW-DLT: Documentation 2 | 3 | Detailed information about all the features available on SW-DLT. 4 | 5 | ## Single Video Download 6 | 7 | The video download option offers two types of downloads: Default and Custom Quality. Videos are saved with the original titles fetched by `yt-dlp`. Both options sort formats with a priority of `res,ext:mp4:m4a,codec:avc:m4a` 8 | 9 | **Default Quality**: videos are downloaded using the following `yt-dlp` format string: 10 | 11 | `best/bestvideo+bestaudio` i.e: 12 | 13 | 1. Best quality available (video and audio stream) 14 | 2. Best quality available (separate video and audio to merge with FFmpeg) 15 | 16 | The default quality is the most reliable way to download videos from websites that might not return media format data to `yt-dlp` or are not in the list of supported websites and need to be downloaded using `yt-dlp`'s generic extractor. This option also mostly avoids using FFmpeg to save battery life (FFmpeg can still be used to correct file issues). 17 | 18 | **Custom Quality**: Custom quality videos are selected based on the `height` of the video image that corresponds to the "quality" selected by the user, and the `fps` chosen. 19 | 20 | The priority of videos to search is as follows: 21 | 22 | 1. `bestvideo[height=X][fps<=Y]+bestaudio` (Exact resolution, closest FPS, unmerged) 23 | 2. `best[height=X][fps<=Y]` (Exact resolution, closest FPS, merged) 24 | 3. `bestvideo[height<=X][fps<=Y]+bestaudio` (Closest resolution, closest FPS, unmerged) 25 | 4. `best[height<=X][fps<=Y]` (Closest resolution, closest FPS, merged) 26 | 5. `bestvideo[height<=X][fps<=Y]+bestaudio` (Closest resolution, closest FPS, unmerged) 27 | 6. `best[height={0}]` (Exact resolution, ignore FPS, merged) 28 | 29 | **What does this all mean?** In general, it means SW-DLT will prioritize above all the resolution option you select, and then prioritize the FPS you select. Among matching results, it will then prioritize those playable natively on iOS/iPadOS 30 | 31 | ## Single Audio Download 32 | 33 | Audio downloads prioritize the best audio available from a website. In case there is no audio only stream, the best muxed video will be used to extract audio from it. Audios are saved with the original titles fetched by `yt-dlp`. 34 | 35 | `bestaudio[ext*=4]/bestaudio[ext=mp3]/best[ext=mp4]/best` 36 | 37 | 1. Best MP4/M4A audio available 38 | 2. Best MP3 audio available 39 | 3. Audio extracted from best MP4 video available 40 | 4. Audio extracted from best video available 41 | 42 | ## Playlist Download 43 | 44 | Playlist downloads support both downloading all items in the playlist as videos or as audio only files. There are no quality options for video playlist downloads currently. Audio playlist downlods can use FFmpeg to correct errors and to extract audio from videos. Playlists are saved with the original titles fetched by `yt-dlp`. Both options prioritize iOS natively playable codec with a priority of `+codec:avc:m4a` 45 | 46 | Video downloads use the formats: `best[ext=mp4]/best` 47 | 48 | 1. Best MP4 video 49 | 2. Best video of any type 50 | 51 | Audio downloads use the formats `bestaudio[ext*=4]/bestaudio[ext=mp3]/best[ext=mp4]/best` 52 | 53 | 1. Best MP4/M4A audio available 54 | 2. Best MP3 audio available 55 | 3. Audio extracted from best MP4 video available 56 | 4. Audio extracted from best video available 57 | 58 | ## Gallery Download 59 | 60 | Gallery downloads are done using `gallery-dl` instead of `yt-dlp`. `gallery-dl` works primarily with images, but can be used to download GIFs and Clips from social media and other hosting websites. `gallery-dl` is able to download items in bulk up to entire websites and user feeds. 61 | 62 | Single item downloads with `gallery-dl` are returned as-is, while multi-item downloads are packaged in a zip archive. Due to lack of proper python API, items are given timestamp names. 63 | 64 | Gallery downloading supports custom download ranges (which are directly passed to `gallery-dl`). 65 | 66 | Example: `1, 2-5, 7, 9-15` 67 | 68 | - Downloads the first item, followed by items 2 to 5, followed by item 7, followed by items 9 to 15 69 | - The first item is usually the newest item in a social media feed 70 | 71 | ## Authentication 72 | 73 | `yt-dlp` and `gallery-dl` are automatically set to use cookies for authentication. This allows for downloading of media that is restricted behind a login page. Use the following steps per authenticated website: 74 | 75 | 1. Open the a-Shell or a-Shell Mini app 76 | 2. Enter command `internalbrowser` followed by the website URL you want to access. For instance, `internalbrowser https://instagram.com`. 77 | 3. A Web View of the website should open within the terminal. In case nothing happens, make sure to include `https://` in the previous step. 78 | 4. Navigate to the login screen of the website and login as you would usually do. 79 | 5. Once you have logged in, swipe from the left until you are back at the terminal screen. Alternatively, you can simply go into the App Switcher and swipe a-Shell away. 80 | 81 | **NOTE:** SW-DLT does not access credentials or cookie data. `yt-dlp` and `gallery-dl` use it directly. 82 | 83 | ## Saving Downloads 84 | 85 | Downloads are NOT automatically saved by SW-DLT due to the many different file types that are supported. All downloads are shown to the user using the share sheet preview. From this preview users are able to choose where to send the downloaded item(s). 86 | 87 | It is recommended to have the VLC app or another universal media player app to enable playing media on unsupported formats for iOS/iPadOS. 88 | 89 | ## Resuming Downloads 90 | 91 | Resuming a download only works if you retry the **LAST** attempted download with the **EXACT** same parameters (URL, quality, type, authentication type, etc). You can verify that a download is resuming if you see "SW-DLT (Resuming Download)" as the header in a-Shell when the program is running. You can stop and resume the same 92 | download as many times as needed, but **if you download something else before completing the partial download, the partial files will be removed**. This ensures no accumulation of more than one partial files in the `$HOME/Documents/SW-DLT` directory. Resuming downloads works with ALL functions of the shortcut. 93 | 94 | Example: 95 | - If you attempted to download video X but interrupted the download, and then you attempt to download video X again, it will be resumed (if the same parameters are used). 96 | - If you attempted to download video X but interrupted the download, **but then you attempt to download video Y**, the files for video X will be cleaned (at this point 97 | video Y will take the place as the video that can be resumed if it's interrupted). 98 | 99 | ## Uninstall the Shortcut 100 | The following steps will allow you to uninstall all files used by SW-DLT, without deleting a-Shell. All files are located in `$HOME/Documents/SW-DLT` 101 | 102 | 1. Tap on the edit Shortcut button 103 | 2. Switch the 'uninstall' toggle from 'False' to 'True' as instructed by comments 104 | 3. Run the Shortcut and accept the prompt 105 | 4. Do not run the shortcut again or else it will be reinstalled. 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 net00 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SW-DLT 2 | 3 | SW-DLT ("Shortcuts Wrapper for -DL Tools") is an iOS shortcut that allows you to easily use, install and manage the popular and open source utilities [`yt-dlp`](https://github.com/yt-dlp/yt-dlp) and [`gallery-dl`](https://github.com/mikf/gallery-dl). This shortcut aims to be compatible with [`yt-dlp's`](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md) & [`gallery-dl's`](https://github.com/mikf/gallery-dl/blob/master/docs/supportedsites.md) collections of supported websites. 4 | 5 | **Download link: [Releases](https://github.com/net00-1/SW-DLT/releases)** 6 |
7 | 8 |
9 | 10 | Compatible with **iOS/iPadOS 17 & 18** 11 | ## How to Use 12 | 1. **Download either the [a-Shell](https://apps.apple.com/us/app/a-shell/id1473805438) or [a-Shell Mini](https://apps.apple.com/us/app/a-shell-mini/id1543537943) app** 13 | 2. Copy a link to the clipboard (or use the share button if applicable) 14 | 3. Run the shortcut from either the Shortcuts app (if using a copied link) or from the Share Sheet 15 | 4. Select the option you need 16 | 5. Wait for the process to finish 17 | 6. Use the Share menu to export the download to your chosen destination 18 | 19 | ## Main Features 20 | **Please check the full [documentation page](https://github.com/net00-1/SW-DLT/blob/master/Docs.md) to see a complete description of the available features** 21 | 22 | - Download video at custom qualities and framerates 23 | - Downaload audio (audio only or extracted from video) 24 | - Download playlists (video or audio choices available) 25 | - Download images/clips/GIFs (from single items to massive albums) 26 | - Support for both a-Shell and a-Shell Mini apps 27 | 28 | ## Build the Shortcut 29 | 1. Download the shortcut plist source code from src/SW-DLT.plist 30 | 2. Change the extension from '.plist' to '.shortcut' 31 | 3. Sign the shortcut with Apple (see "Ways to sign shortcuts" section below) 32 | 4. Import the signed .shortcut file to the Shortcuts app 33 | 34 | **Ways to sign shortcuts** 35 | - From a Mac: follow [Apple documentation](https://support.apple.com/guide/shortcuts-mac/run-shortcuts-from-the-command-line-apd455c82f02/mac) 36 | - From iPad/iPhone: use a tool to connect remotely to a Mac, such as [Shortcuts Source Helper](https://routinehub.co/shortcut/10060/). Credits to [@gluebyte](https://routinehub.co/user/gluebyte). If using this tool, pass the **unmodified plist (without extension change)** as input. 37 | 38 | **NOTE**: if you do not want to use a remote service to sign the shortcut, nor have access to a Mac, you can still preview the source code as Shortcuts actions. Follow the first two steps of build instructions, then from the iPhone/iPad Files app hold on the .shortcut file, then press on the QuickLook option. 39 | 40 | ## Roadmap Features 41 | 42 | - [ ] Implement full progress bar on gallery-dl downloads 43 | 44 | - [ ] Add quality options for video playlists 45 | 46 | - [ ] Add option to customize yt-dlp and gallery-dl with persistent settings 47 | 48 | - [ ] Allow file inputs with multiple URLs 49 | 50 | ## Disclaimers 51 | - Use this shortcut for downloading media you own or are authorized to download 52 | - All software used (`yt-dlp`, `gallery-dl`, SW-DLT, a-Shell) is open source and free 53 | - It is recommended to have the VLC app or another universal media player app to play unsupported media formats in iOS/iPadOS. 54 | -------------------------------------------------------------------------------- /images/all_stages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/net00-1/SW-DLT/e1f9ed1c77c997783d8c8fefcdd6ee6f0f51449f/images/all_stages.png -------------------------------------------------------------------------------- /images/donation_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/net00-1/SW-DLT/e1f9ed1c77c997783d8c8fefcdd6ee6f0f51449f/images/donation_qr.png -------------------------------------------------------------------------------- /src/SW_DLT.py: -------------------------------------------------------------------------------- 1 | # SW-DLT script, check Github for documentation. 2 | # Official release on GitHub, avoid unknown sources 3 | 4 | import urllib.parse 5 | import contextlib 6 | import subprocess 7 | import datetime 8 | import hashlib 9 | import shutil 10 | import base64 11 | import yt_dlp 12 | import json 13 | import sys 14 | import os 15 | 16 | # Constants class 17 | class Consts: 18 | CYELLOW, CGREEN, CBLUE, SBOLD, ENDL = "\033[93m", "\033[92m", "\033[94m", "\033[1m", "\033[0m" 19 | DERROR_EXC = '{"output_code":"exception","exc_trace":"vars.downloadError"}' 20 | SET_COOKIE = "echo 'document.cookie = \"installed=1; expires=Thu, 1 Jan 2026 12:00:00 UTC; sameSite=Lax\";' | jsi" 21 | 22 | 23 | class SW_DLT: 24 | 25 | def __init__(self, file_id, *args): 26 | # args[0]: media URL to download 27 | # args[1]: main process to run 28 | # args[2] (dependent): resolution for video, type for playlist, or range for gallery 29 | # args[3] (dependent): framerate for video 30 | self.media_url = args[0] 31 | self.file_id = file_id 32 | self.date_id = datetime.datetime.today().strftime("%d-%m-%y-%H-%M-%S") 33 | self.ytdlp_globals = { 34 | "color": "never", 35 | "quiet": True, 36 | "no_warnings": True, 37 | "noprogress": True, 38 | "progress_hooks": [show_progress], 39 | "postprocessor_hooks": [format_processing], 40 | "cookiesfrombrowser": ("safari",) 41 | } 42 | 43 | processes = { 44 | "-v": self.single_video, 45 | "-a": self.single_audio, 46 | "-p": self.playlist_download, 47 | "-g": self.gallery_download 48 | } 49 | self.run = processes[args[1]] 50 | self.video_res = "" 51 | self.video_fps = "" 52 | self.playlist_type = "" 53 | self.gallery_range = "" 54 | 55 | if len(args) > 2: 56 | self.video_res = args[2] if args[1] == "-v" else "" 57 | self.playlist_type = args[2] if args[1] == "-p" else "" 58 | self.gallery_range = args[2].replace('"','').replace("'","") if args[1] == "-g" else "" 59 | 60 | if len(args) > 3: 61 | self.video_fps = args[3] 62 | 63 | @staticmethod 64 | def update_check(): 65 | show_progress("util", 0, 2) 66 | if not os.path.exists(f"{os.environ['HOME']}/Library/Cookies/Cookies.binarycookies"): 67 | subprocess.run(Consts.SET_COOKIE) 68 | 69 | #We need to wait for delay in jsi command 70 | while not os.path.exists(f"{os.environ['HOME']}/Library/Cookies/Cookies.binarycookies"): 71 | subprocess.run("sleep 1") 72 | 73 | show_progress("util", 1, 2) 74 | current_time = int(datetime.datetime.today().timestamp()) 75 | 76 | with open(f"{os.environ['HOME']}/Documents/SW-DLT/shortcut_update_ts.txt", 'r') as ts_file: 77 | last_check = int(ts_file.read()) 78 | 79 | if current_time - last_check < 600: 80 | subprocess.run("pip -q install gallery-dl yt-dlp --disable-pip-version-check --upgrade") 81 | 82 | show_progress("util", 2, 2) 83 | 84 | def single_video(self): 85 | default_format = "best/bestvideo+bestaudio" 86 | custom_format = ""\ 87 | "bestvideo[height={0}][fps<={1}]+bestaudio/"\ 88 | "best[height={0}][fps<={1}]/"\ 89 | "bestvideo[height<={0}][fps<={1}]+bestaudio/"\ 90 | "best[height<={0}][fps<={1}]"\ 91 | "best[height={0}]".format(self.video_res, self.video_fps) 92 | 93 | dl_options = { 94 | "format": default_format if self.video_res == "-d" else custom_format, 95 | "playlist_items": "1-1", 96 | "outtmpl": f'{self.file_id}.%(ext)s', 97 | "format_sort": ["res", "ext:mp4:m4a", "codec:avc:m4a"], 98 | **self.ytdlp_globals 99 | } 100 | 101 | try: 102 | # Returns shortcuts redirect URL with downloaded file data, any exception is re-thrown 103 | return self.single_download(dl_options) 104 | 105 | except (yt_dlp.utils.DownloadError, OSError) as ex: 106 | raise Exception(ex.args[0]) 107 | 108 | def single_audio(self): 109 | dl_options = { 110 | "format": "bestaudio[ext*=4]/bestaudio[ext=mp3]/best[ext=mp4]/best", 111 | "playlist_items": "1-1", 112 | "postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": "m4a"}], 113 | "outtmpl": f'{self.file_id}.%(ext)s', 114 | **self.ytdlp_globals 115 | } 116 | 117 | try: 118 | # Returns shortcuts redirect URL with downloaded file data, any exception is re-thrown 119 | return self.single_download(dl_options) 120 | 121 | except (yt_dlp.utils.DownloadError, OSError) as ex: 122 | raise Exception(ex.args[0]) 123 | 124 | def single_download(self, dl_options): 125 | # Uses yt-dlp to download single video or audio items 126 | with yt_dlp.YoutubeDL(dl_options) as vid_obj: 127 | meta_data = vid_obj.extract_info(self.media_url, download=False) 128 | vid_title = meta_data.get("title", self.date_id) 129 | vid_obj.download([self.media_url]) 130 | 131 | for file in os.listdir(): 132 | if file.startswith(self.file_id): 133 | output = { 134 | "output_code": "success", 135 | "file_name": os.path.abspath(file), 136 | "file_title": vid_title 137 | } 138 | return f'shortcuts://run-shortcut?name=SW-DLT&input=text&text={urllib.parse.quote(json.dumps(output))}' 139 | # If for some reason the above doesn't find the downloaded file, we raise generic Exception 140 | raise Exception(Consts.DERROR) 141 | 142 | def gallery_download(self): 143 | try: 144 | # Creating temp folder to store media 145 | os.makedirs(self.file_id, exist_ok=True) 146 | dl_cmd = "gallery-dl {0} --range \"{1}\" --directory {2} --cookies-from-browser safari".format( 147 | self.media_url, self.gallery_range, self.file_id) 148 | 149 | with subprocess.Popen(dl_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1, universal_newlines=True) as gdl: 150 | for entry in gdl.stdout: 151 | show_progress("manual", 1, 1) 152 | 153 | files = os.listdir(self.file_id) 154 | # No files returned, removes temp folder and raises Exception 155 | if len(files) == 0: 156 | shutil.rmtree(self.file_id, True) 157 | raise OSError() 158 | 159 | # Single item, removes temp folder and directly outputs the item 160 | elif len(files) < 2: 161 | file = "{0}/{1}".format(self.file_id, files[0]) 162 | output = { 163 | "output_code": "success", 164 | "file_name": os.path.abspath(file), 165 | "file_title": self.date_id 166 | } 167 | 168 | # Mutiple items, zips temp folder and returns it, removes temp folder 169 | else: 170 | shutil.make_archive(self.file_id, "zip", self.file_id) 171 | shutil.rmtree(self.file_id, True) 172 | output = { 173 | "output_code": "success", 174 | "file_name": os.path.abspath(self.file_id + ".zip"), 175 | "file_title": self.date_id 176 | } 177 | 178 | return f'shortcuts://run-shortcut?name=SW-DLT&input=text&text={urllib.parse.quote(json.dumps(output))}' 179 | except subprocess.CalledProcessError as ex: 180 | clean_stderr = ex.output.decode("utf-8").rstrip() 181 | raise Exception(clean_stderr) 182 | except (AttributeError, OSError) as ex2: 183 | raise Exception(Consts.DERROR_EXC) 184 | 185 | def playlist_download(self): 186 | dl_options = { 187 | "format": "best" if self.playlist_type == "-v" else "bestaudio[ext*=4]/bestaudio[ext=mp3]/best[ext=mp4]/best", 188 | "postprocessors": [] if self.playlist_type == "-v" else [{"key": "FFmpegExtractAudio", "preferredcodec": "m4a"}], 189 | "outtmpl": f'{self.file_id}/%(title)s.%(ext)s', 190 | "format_sort": ["+codec:avc:m4a"], 191 | **self.ytdlp_globals 192 | } 193 | 194 | try: 195 | with yt_dlp.YoutubeDL(dl_options) as pl_obj: 196 | meta_data = pl_obj.extract_info(self.media_url, download=False) 197 | pl_title = meta_data.get("title", self.date_id) 198 | pl_obj.download([self.media_url]) 199 | 200 | shutil.make_archive(self.file_id, "zip", self.file_id) 201 | shutil.rmtree(self.file_id, True) 202 | output = { 203 | "output_code": "success", 204 | "file_name": os.path.abspath(self.file_id + ".zip"), 205 | "file_title": pl_title 206 | } 207 | return f'shortcuts://run-shortcut?name=SW-DLT&input=text&text={urllib.parse.quote(json.dumps(output))}' 208 | 209 | except (yt_dlp.utils.DownloadError, OSError) as ex: 210 | raise Exception(ex.args[0]) 211 | 212 | 213 | def show_progress(data_stream, curr=0, total=0): 214 | # data_stream type of data received, can be manual (for gallery-dl downloads), util (for utility processes) 215 | # it can also be yt-dlp download data streams 216 | if data_stream == "manual": 217 | if curr != total: 218 | print( 219 | f'\rDownloading: {Consts.CYELLOW}{curr/total:.1%}{Consts.ENDL}', end="") 220 | return 221 | print(f'\x1b[1K\r{Consts.CGREEN}Downloaded{Consts.ENDL}') 222 | elif data_stream == "util": 223 | print( 224 | f'\rLoading: {Consts.CYELLOW}{curr/total:.1%}{Consts.ENDL}', end="") 225 | return 226 | else: 227 | if data_stream["status"] == "downloading": 228 | print( 229 | f"\rDownloading: {Consts.CYELLOW}{data_stream['_percent_str'].strip()}{Consts.ENDL}", end="") 230 | elif data_stream["status"] == "finished": 231 | print(f'\x1b[1K\r{Consts.CGREEN}Downloaded{Consts.ENDL}') 232 | return 233 | 234 | 235 | def format_processing(process_stream): 236 | if process_stream["status"] == "started": 237 | print(f'\r{Consts.CYELLOW}Processing{Consts.ENDL}', end="") 238 | elif process_stream["status"] == "finished": 239 | print(f'\x1b[1K\r{Consts.CGREEN}Processed{Consts.ENDL}') 240 | return 241 | 242 | 243 | def main(): 244 | info_msgs = { 245 | "-v": f'{Consts.CBLUE}Video Download{Consts.ENDL}\n{Consts.CYELLOW}Custom qualities require processing{Consts.ENDL}', 246 | "-a": f'{Consts.CBLUE}Audio Download{Consts.ENDL}\n{Consts.CYELLOW}Sometimes audio processing is needed{Consts.ENDL}', 247 | "-p": f'{Consts.CBLUE}Playlist Download{Consts.ENDL}\n{Consts.CYELLOW}Process time depends on playlist length{Consts.ENDL}', 248 | "-g": f'{Consts.CBLUE}Gallery Download{Consts.ENDL}\n{Consts.CYELLOW}Process time depends on collection length{Consts.ENDL}', 249 | "-e": f'{Consts.CYELLOW}Deleting All Dependencies{Consts.ENDL}', 250 | "update_check": f'{Consts.CBLUE}Preparing{Consts.ENDL}\n{Consts.CYELLOW}Checking for Updates{Consts.ENDL}' 251 | } 252 | try: 253 | # Hashes all arguments to generate unique ID 254 | file_id = "SW_DLT_DL_{}".format(hashlib.md5( 255 | str(sys.argv).encode("utf-8")).hexdigest()[0:20]) 256 | 257 | sw_dlt_inst = SW_DLT(file_id, *sys.argv[1:]) 258 | header = f'{Consts.SBOLD}SW-DLT{Consts.ENDL}' 259 | 260 | # Pre-download check and cleanup 261 | subprocess.run("clear") 262 | 263 | print(header) 264 | print(info_msgs["update_check"]) 265 | sw_dlt_inst.update_check() 266 | 267 | # If the same partial file is not found, deletes all leftovers (important) 268 | for file in os.listdir(): 269 | if file.startswith("SW_DLT_DL_") and not file.startswith(file_id): 270 | if os.path.isdir(file): 271 | shutil.rmtree(file) 272 | continue 273 | os.remove(file) 274 | elif file.startswith(file_id): 275 | header = f'{Consts.SBOLD}SW-DLT (Resuming Download){Consts.ENDL}' 276 | 277 | subprocess.run("clear") 278 | print(header) 279 | print(info_msgs[sys.argv[2]]) 280 | 281 | with open('SW_DLT_DL_metadata.json', 'w') as metadata: 282 | args = ' '.join(map(str, sys.argv[2:])) 283 | json.dump({f"{sys.argv[1]}": args}, metadata) 284 | 285 | return sw_dlt_inst.run() 286 | 287 | except Exception as exc_url: 288 | # All raised exceptions are handled here and send the user back to the shortcut with a message 289 | if str(exc_url.args[0]) not in [Consts.DERROR_EXC]: 290 | b64_err = base64.b64encode(exc_url.args[0].encode()).decode() 291 | UNK_EXC = '{{"output_code":"exception","exc_trace":"{0}"}}'.format(b64_err) 292 | return f'shortcuts://run-shortcut?name=SW-DLT&input=text&text={urllib.parse.quote(UNK_EXC)}' 293 | return f'shortcuts://run-shortcut?name=SW-DLT&input=text&text={urllib.parse.quote(str(exc_url.args[0]))}' 294 | 295 | 296 | if __name__ == "__main__": 297 | subprocess.run("open " + main()) 298 | # Post-run cleanup 299 | subprocess.run("deactivate") 300 | subprocess.run("clear") 301 | --------------------------------------------------------------------------------