├── .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 |
--------------------------------------------------------------------------------