Skip to content

Commit

Permalink
Refactoring + presets support
Browse files Browse the repository at this point in the history
Presets
  • Loading branch information
Totonyus authored Sep 18, 2020
2 parents b713b67 + fb7db28 commit e9dbc1b
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 164 deletions.
128 changes: 38 additions & 90 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,102 +1,50 @@
import logging, params, youtube_dl
import logging, ydl_utils, params
from urllib.parse import urlparse, unquote
from fastapi import BackgroundTasks, FastAPI, Response

app = FastAPI()

# used to define if the url is a video, un playlist or a video in a playlist
def define_properties(url):
properties = {"playlist" : False, "video" : False} # set at the beginning in case params.playlist_detection is empty
@app.get(params.api_route)
async def download_request(response : Response, background_tasks : BackgroundTasks, url,
format = None, subtitles = None, location = None, filename = None, presets = None):

for entry in params.playlist_detection:
properties = {"playlist" : False, "video" : False} # reset evety loop

for indicator in entry['video_indicators']:
properties['video'] = True if url.find(indicator) != -1 else properties['video']

for indicator in entry['playlist_indicators']:
properties['playlist'] = True if url.find(indicator) != -1 else properties['playlist']

return properties

def must_be_checked(url, no_playlist = params.no_playlist):
properties = define_properties(url)
is_a_playlist = properties['playlist']
is_a_video = properties['video']

# To avoid failing a test for ONE video impossible to download in the entire playlist
if is_a_video and ((not is_a_playlist) or (is_a_playlist and no_playlist)) :
return True
elif is_a_playlist and ((not is_a_video) or (is_a_video and not no_playlist)):
return False
else: #In other cases : checking
return True

### Verify if youtube-dl can find video and the format is right
def check_download(url, format):
ydl_opts = { # the option ignoreerrors breaks the function but it can be a problem while downloading playlists with unavailable videos inside
'quiet': True,
'simulate': True,
'format': format,
'noplaylist': params.no_playlist
}

with youtube_dl.YoutubeDL(ydl_opts) as ydl:
try:
if must_be_checked(url):
logging.info("Checking download")
ydl.download([url])
return {'checked' : True, 'errors' : False}
else:
logging.warning("Unable to check download")
return {'checked' : False, 'errors' : False}
except:
return {'checked' : True, 'errors' : True}

### Launch the download instruction
def launch_download(url, ydl_opts):
logging.info(f"Downloading '{url}' with quality '{ydl_opts['format']}' in '{ydl_opts['outtmpl']}'")

with youtube_dl.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])

@app.get("/download")
async def create_download(response : Response, background_tasks : BackgroundTasks, url: str,
format: str = params.default_format, subtitles : str = params.default_subtitles_languages, location: str = "default"):
decoded_url = unquote(url)
decoded_format = unquote(format)
decoded_subtitles = subtitles.split(',') if subtitles is not None else None

# used to pass useful vars for naming purpose
ydl_api_opts = {
'url': decoded_url,
'hostname' : urlparse(decoded_url).hostname,
'location_identifier' : location
}

download_dir = params.download_dir(ydl_api_opts)

ydl_opts = {
'quiet': True,
'ignoreerrors' : True,
'outtmpl': download_dir + params.file_name_template(ydl_api_opts),
'format': decoded_format,
'noplaylist': params.no_playlist,
'writesubtitles': subtitles is not None,
'subtitleslangs' : decoded_subtitles,
'subtitlesformat': params.default_subtitles_format
decoded_presets = [] # from string to list
selected_presets_objects = [] # store presets objects required by the presets field

if presets is not None:
decoded_presets = presets.split(',') if presets is not None else None
selected_presets_objects = ydl_utils.existing_presets(decoded_presets) # transform string in object

query_parameters = { # parameters object build form url query parameters
'format' : unquote(format) if format is not None else None,
'subtitles' : unquote(subtitles) if subtitles is not None else None,
'location' : unquote(location) if location is not None else None,
'filename' : unquote(filename) if filename is not None else None,
'presets' : unquote(presets) if presets is not None else None
}

checked_download = check_download(url, decoded_format)

if checked_download['checked'] is False:
background_tasks.add_task(launch_download, decoded_url, ydl_opts)
response.status_code = 202
elif checked_download['checked'] and checked_download['errors'] is False:
background_tasks.add_task(launch_download, decoded_url, ydl_opts)
response.status_code = 200
# generate all options sets for all download
downloads_options_sets = ydl_utils.generate_ydl_options_sets(decoded_url, selected_presets_objects, query_parameters)

# count the number of check downloads and the number of errors
validity_check = ydl_utils.recap_all_downloads_validity(downloads_options_sets)

# if all downloads were checked and without errors, we can ensure the file will be correctly downloaded
if validity_check.get('checked') == validity_check.get('total') and validity_check.get('errors') == 0:
background_tasks.add_task(ydl_utils.launch_downloads, decoded_url, downloads_options_sets)
response.status_code = 200 # request ok
# if not all downloads were checked, we can't ensure all files will be correctly downloaded
elif validity_check.get('checked') != validity_check.get('total'):
background_tasks.add_task(ydl_utils.launch_downloads, decoded_url, downloads_options_sets)
response.status_code = 202 # request ok but result not granted
# if all downloads are in error, we can ensure no file will be downloaded
else:
logging.error(f"Impossible to download '{decoded_url}'")
response.status_code = 400
response.status_code = 400 # bad request

return {'url' : decoded_url, 'format': decoded_format, 'download_dir' : download_dir, 'status' : response.status_code, 'subtitles' : decoded_subtitles, 'checked_download' : checked_download}
return {
'url' : decoded_url,
'presets_errors' : (len(decoded_presets) - len(selected_presets_objects)),
'list' : downloads_options_sets
}
63 changes: 29 additions & 34 deletions params.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,35 @@
import logging
from datetime import date
api_route = "/download"

### Change here the default format to use : https://github.com/ytdl-org/youtube-dl/tree/3e4cedf9e8cd3157df2457df7274d0c842421945#format-selection
default_format="bestvideo+bestaudio/best"
default_download_format= "bestvideo+bestaudio/best" #https://github.com/ytdl-org/youtube-dl/tree/3e4cedf9e8cd3157df2457df7274d0c842421945#format-selection

### Example : "en,fr", None = don't download subtitles
default_subtitles_languages = None

### if not possible, it will download any available format
default_subtitles_languages = None #example : "en,fr"
default_subtitles_format = "srt"

### equivalent of --no-playlist option : if the playlist is in the url True = download only the current video, False = download the whole playlist
no_playlist = True

### if present in url, the url is a playlist.
playlist_detection = [
{'video_indicators': ['/watch?'] , 'playlist_indicators' : ['?list=', '&list=']}, #preset for youtube
no_playlist = True #prevent downloading all the playlist when a video url contains playlist id
playlist_detection = [ #used to detect if the url is a video, a playlist or a video in a playlist
{'video_indicators': ['/watch?'] , 'playlist_indicators' : ['?list=', '&list=']} #youtube
]

### Change here the download directory and the file name : https://github.com/ytdl-org/youtube-dl/tree/3e4cedf9e8cd3157df2457df7274d0c842421945#output-template
# provided vars
# ydl_api_opts = {'url', 'hostname', 'location_identifier' }
def file_name_template(ydl_api_opts):
return "%(title)s_(%(height)s).%(ext)s"

# Multiple choices with the parameter &location
def download_dir(ydl_api_opts):
locations={ ###- --%--- REPLACE ---%--- here with your different download directorues
'default' : f"{ydl_api_opts.get('hostname')}/",
#'date' : f"{date.today().strftime('%Y_%m_%d')}/" # for example : &location=date
}

if locations.get(ydl_api_opts.get('location_identifier')) is not None:
location = locations.get(ydl_api_opts.get('location_identifier'))
else:
logging.warning(f"{ydl_api_opts.get('location_identifier')} identifier not found. Using the default one instead")
location = locations.get('default')

return location
root_download_directory = "downloads"
# https://github.com/ytdl-org/youtube-dl/tree/3e4cedf9e8cd3157df2457df7274d0c842421945#output-template
# you can use those tags : %hostname%, %location_identifier%, %filename_identifier%
download_directory_templates={ # you must keep a 'default' preset
'default' : f"{root_download_directory}/videos/%hostname%/",
'audio' : f"{root_download_directory}/audio/"
}

# https://github.com/ytdl-org/youtube-dl/tree/3e4cedf9e8cd3157df2457df7274d0c842421945#output-template
# you can use those tags : %hostname%, %location_identifier%, %filename_identifier%
file_name_templates = { # you must keep a 'default' preset
'default' : "%(title)s_(%(height)s).%(ext)s",
'audio' : "%(title)s.%(ext)s",
}

presets_templates={ # you must keep a 'default' preset
'default' : {'format' : default_download_format, 'subtitles' : default_subtitles_languages, 'location' : 'default', 'filename' : 'default'},
'audio': {'format' : 'bestaudio', 'subtitles' : default_subtitles_languages, 'location' : 'audio', 'filename' : 'audio'},
'best' : {'format' : 'bestvideo+bestaudio/best', 'subtitles' : default_subtitles_languages, 'location' : 'default', 'filename' : 'default'},
'fullhd' : {'format' : 'best[height=1080]/best', 'subtitles' : default_subtitles_languages, 'location' : 'default', 'filename' : 'default'},
'hd' : {'format' : 'best[height=720]/best', 'subtitles' : default_subtitles_languages, 'location' : 'default', 'filename' : 'default'},
'sd' : {'format' : 'best[height=360]/best', 'subtitles' : default_subtitles_languages, 'location' : 'default', 'filename' : 'default'},
}
103 changes: 84 additions & 19 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

### Exhaustive list of the ydl_api features
- Launch youtube-dl download directly on your server
- Choice your video format
- Chose your video format
- Download subtitles

### Disclaimer
Expand All @@ -11,27 +11,40 @@ This is my very first python program. I did my best. If you are an experienced p
### Files
* `readme.md`
* `main.py` the main program
* `params.py`
* `params.py` all the default parameters of the application, everything is set up to offer you a working application out of the box
* `launch.sh` a simple sh file to launch the server
* `userscript.js` a javascript file you can import in [Greasemonkey (firefox)](https://addons.mozilla.org/fr/firefox/addon/greasemonkey/) or [Tampermonkey (chrome)](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=fr) to access the api from yout browser
* `ydl_api.service` a systemd service file
* `ydl_api.service` a systemd service file to launch this application as a daemon
* An iOS shortcut you can find [here](https://www.icloud.com/shortcuts/055260591bfe4e6a837df90864705739)

### Dependancies
### Dependencies
* Installation with distribution package manager (`apt`, `yum`, ...) : `python3`, `python3-pip`, `ffmpeg`
* Installation with pip : `fastapi`, `uvicorn`, `youtube-dl`
* Installation with pip3 : `fastapi`, `uvicorn`, `youtube-dl`

```
pip3 install fastapi youtube-dl uvicorn
```

* Make sure you have the last youtube-dl version installed : `youtube-dl --version` (currently `2020.09.06`). You could have problems with some videos if you use an older version.

### Installation
#### Download this repo
Download this git repository using git command or download the zip with wget command. Then unzip the content where you want.
Download the latest release :
```
wget https://github.com/Totonyus/ydl_api/archive/master.zip
```

Then unzip the file and rename the unziped folder : (note you can use `unzîp -d %your_path%` to specify where you want to unzip the file )
```
unzip master.zip; mv ydl_api-master.zip ydl_api
```

#### Configuration
The application is setup to work out of th box but you should probably change some settings :

#### Replace placeholders
Placeholders looks like this : `---%--- REPLACE ---%---`
* (Optional) `params.py` : you can change destination folder, file name template and a few others options
* (Optional) `userscript.js` : you must set your host, you can also add format options as you want
* (Optional) `userscript.js` : you must set your api route (default : `http://localhost:5011/download`, you can also add format options as you want
* (Optional) `launch.sh` : the default port is set arbitrarily to `5011`. Change it as you want.
* (Optional) `ydl_api.service` : you must set the working directory to the directory you downloaded this application. If you don't want change default user and group, you can delete those lines :

Expand Down Expand Up @@ -79,24 +92,76 @@ systemctl disable ydl_api
#### Install the userscript
Install [Greasemonkey (firefox)](https://addons.mozilla.org/fr/firefox/addon/greasemonkey/) or [Tampermonkey (chrome)](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=fr) and add a new userscript. Then copy the content of `userscript.js` in the interface and save.

You now should have access to download options on every sites.
You now should have access to download options on every site.

![result.jpg](result.jpg)

### Use API
Simple exemple of request to use anywhere you want :
##### Userscript setup
You probably should change the default host set in the script.

```
GET : http://%HOST%:%PORT%/download?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ&format=best[height=720]/best&subtitles=fr,en&location=date
```
You can modify the `preset_list` as you want. Note : the key parameter of a preset is not mandatory. It's just a shortcut.

### API usage
#### Templates
You can create three types of templates :

* download destinations in `params.download_directory_templates`
* file names in `params.file_name_templates`
* server-side presets in `params.presets_templates`

In all cases, always keep a template named `default` (you still can modify them).

##### Download destinations and file name templates
In those templates, you can use tags delimited by `%` (example : `%hostname%`).

Provided tags are `hostname`, `location_identifier` and `filename_identifier`

##### Presets
If more than one preset is provided (`&presets=audio,best`), the url will be downloaded one time with each format. For example, you can download both music and clip of a song video.

Some rules :
* If not all provided presets are correct, only the correct presets will be downloaded
* If no correct preset is provided, the default preset is used

#### Query parameters
Parameters :
* `url` : the page to download
* (optional) `format` : the format of the video you want, if not provided, default value = `params.default_format`. TODO : verify in the API if the format is correct
* (optional) `format` : the format of the video you want, if not provided, default value = `params.default_format`
* (optional) `subtitles` : the list of subtitles you want to download. Can not download generated subtitles. If not provided, default value = `params.default_subtitles_languages`
* (optional) `location` : the identifier of the location you want to use. Set in `params.download_dir`. If not provided, default value = `default`
* (optional) `location` : the identifier of the location you want to use. Set in `download_directory_templates`. If not provided, default value = `default`
* (optional) `filename` : the identifier of the filename you want to use. Set in `params.file_name_templates`. If not provided, default value = `default`
* (optional) `presets` : the identifier of the presets you want to use. Multiple preset are separated by coma `&presets=audio,best`. The presets are defines in `params.presets_templates`

#### Priority
The priority order of parameters is : Query paramaters > Preset parameters > Default parameters.

This means :
* you can override a preset parameter in your query
* if a parameter is not present in you preset, the parameter of the default preset will be used (unless the parameter is present il query)

#### API usage examples
Only the url is required to use the api.

The simplest request is : (will use the default preset)

```
GET http://localhost:5011/download?url=https://www.youtube.com/watch?v=9Lgc3TxqgHA
```

However, most of the requests defined in the userscript will look like hits

### ios shortcut
Find here the shortcut created to use with ydl_api : [https://www.icloud.com/shortcuts/f2719de8ea47404594a09cd1bcdc5d48](https://www.icloud.com/shortcuts/f2719de8ea47404594a09cd1bcdc5d48)
```
GET http://localhost:5011/download?url=https://www.youtube.com/watch?v=Kf1XttuuIiQ&presets=audio
```

You can override presets parameters
```
GET http://localhost:5011/download?url=https://www.youtube.com/watch?v=wV4wepiucf4&presets=audio&location=default
```

Only the `url` is provided to the api. All other fields uses default values of the server.
#### API response
Responses status :
* 200 : Everything should be downloaded correctly
* 202 : When downloading playlist : not all downloads were checked, some file may not be downloaded by youtube-dl
* 202 : When using multiple presets : one or more presets is invalid, not all files will be downloaded
* 400 : No video can be downloaded
Loading

0 comments on commit e9dbc1b

Please sign in to comment.