mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-11 23:30:58 +03:00
Compare commits
30 Commits
categories
...
youtube-su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c28f8dd48 | ||
|
|
8938844ffa | ||
|
|
9895d77e01 | ||
|
|
27437a615f | ||
|
|
b730bc5adc | ||
|
|
d15d262b87 | ||
|
|
1aade1202d | ||
|
|
2f541a49df | ||
|
|
d93481640c | ||
|
|
71814cbdc9 | ||
|
|
09832ad15b | ||
|
|
cc78091403 | ||
|
|
cb88c7bc7c | ||
|
|
98f4828db4 | ||
|
|
8f0739c0f9 | ||
|
|
ab355d62a0 | ||
|
|
4d2d9a6b10 | ||
|
|
89dfac1249 | ||
|
|
d4f81eb0ab | ||
|
|
6b7d0681d2 | ||
|
|
b32fdb2445 | ||
|
|
b059c7ed5e | ||
|
|
8d87cbb08d | ||
|
|
1bb2f54eba | ||
|
|
7392338d6e | ||
|
|
82df92a72d | ||
|
|
9e4b328f91 | ||
|
|
3a049a99ac | ||
|
|
b323b548ca | ||
|
|
568463487f |
92
.github/workflows/build.yml
vendored
Normal file
92
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: continuous integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, feat/*]
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v1
|
||||
- name: install dependencies
|
||||
run: |
|
||||
npm install
|
||||
cd backend
|
||||
npm install
|
||||
sudo npm install -g @angular/cli
|
||||
- name: build
|
||||
run: ng build --prod
|
||||
- name: prepare artifact upload
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -Name build -ItemType Directory
|
||||
New-Item -Path build -Name youtubedl-material -ItemType Directory
|
||||
Copy-Item -Path ./backend/appdata -Recurse -Destination ./build/youtubedl-material
|
||||
Copy-Item -Path ./backend/audio -Recurse -Destination ./build/youtubedl-material
|
||||
Copy-Item -Path ./backend/authentication -Recurse -Destination ./build/youtubedl-material
|
||||
Copy-Item -Path ./backend/public -Recurse -Destination ./build/youtubedl-material
|
||||
Copy-Item -Path ./backend/subscriptions -Recurse -Destination ./build/youtubedl-material
|
||||
Copy-Item -Path ./backend/video -Recurse -Destination ./build/youtubedl-material
|
||||
New-Item -Path ./build/youtubedl-material -Name users
|
||||
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
|
||||
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
|
||||
- name: upload build artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: youtubedl-material
|
||||
path: build
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: contains(github.ref, '/tags/v')
|
||||
steps:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: YoutubeDL-Material ${{ github.ref }}
|
||||
body: |
|
||||
# New features
|
||||
# Minor additions
|
||||
# Bug fixes
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: download build artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: youtubedl-material
|
||||
path: ${{runner.temp}}/youtubedl-material
|
||||
- name: prepare release asset
|
||||
shell: pwsh
|
||||
run: Compress-Archive -Path ${{runner.temp}}/youtubedl-material -DestinationPath youtubedl-material-${{ github.ref }}.zip
|
||||
- name: upload build asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./youtubedl-material-${{ github.ref }}.zip
|
||||
asset_name: youtubedl-material-${{ github.ref }}.zip
|
||||
asset_content_type: application/zip
|
||||
- name: upload docker-compose asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./docker-compose.yml
|
||||
asset_name: docker-compose.yml
|
||||
asset_content_type: text/plain
|
||||
29
.github/workflows/docker.yml
vendored
Normal file
29
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: setup platform emulator
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: setup multi-arch docker build
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: build & push images
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm,linux/arm64/v8
|
||||
push: true
|
||||
tags: tzahi12345/youtubedl-material:nightly
|
||||
35
README.md
35
README.md
@@ -1,10 +1,10 @@
|
||||
# YoutubeDL-Material
|
||||
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
||||
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
||||
[](https://heroku.com/deploy?template=https://github.com/Tzahi12345/YoutubeDL-Material)
|
||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
|
||||
|
||||
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
||||
[](https://hub.docker.com/r/tzahi12345/youtubedl-material)
|
||||
[](https://heroku.com/deploy?template=https://github.com/Tzahi12345/YoutubeDL-Material)
|
||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
|
||||
[](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
|
||||
|
||||
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 9](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
||||
|
||||
@@ -30,13 +30,25 @@ Dark mode:
|
||||
|
||||
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
|
||||
|
||||
Make sure you have these dependencies installed on your system: nodejs and youtube-dl. If you don't, run this command:
|
||||
Debian/Ubuntu:
|
||||
|
||||
```bash
|
||||
sudo apt-get install nodejs youtube-dl ffmpeg
|
||||
```
|
||||
sudo apt-get install nodejs youtube-dl
|
||||
|
||||
CentOS 7:
|
||||
|
||||
```bash
|
||||
sudo yum install epel-release
|
||||
sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm
|
||||
sudo yum install centos-release-scl-rh
|
||||
sudo yum install rh-nodejs12
|
||||
scl enable rh-nodejs12 bash
|
||||
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
|
||||
```
|
||||
|
||||
Optional dependencies:
|
||||
|
||||
* AtomicParsley (for embedding thumbnails, package name `atomicparsley`)
|
||||
|
||||
### Installing
|
||||
@@ -75,14 +87,16 @@ If you are looking to setup YoutubeDL-Material with Docker, this section is for
|
||||
|
||||
1. Run `curl -L https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/docker-compose.yml -o docker-compose.yml` to download the latest Docker Compose, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
|
||||
2. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
|
||||
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
|
||||
4. Make sure you can connect to the specified URL + port, and if so, you are done!
|
||||
3. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 17443" or something similar. This tells you the *container-internal* port of the application. Please check your `docker-compose.yml` file for the *external* port. If you downloaded the file as described above, it defaults to **8998**.
|
||||
4. Make sure you can connect to the specified URL + *external* port, and if so, you are done!
|
||||
|
||||
NOTE: It is currently recommended that you use the `nightly` tag on Docker. To do so, simply update the docker-compose.yml `image` field so that it points to `tzahi12345/youtubedl-material:nightly`.
|
||||
|
||||
### Custom UID/GID
|
||||
|
||||
By default, the Docker container runs as non-root with UID=1000 and GID=1000. To set this to your own UID/GID, simply update the `environment` section in your `docker-compose.yml` like so:
|
||||
|
||||
```
|
||||
```yml
|
||||
environment:
|
||||
UID: YOUR_UID
|
||||
GID: YOUR_GID
|
||||
@@ -109,6 +123,7 @@ If you're interested in translating the app into a new language, check out the [
|
||||
* **Isaac Grynsztein** (me!) - *Initial work*
|
||||
|
||||
Official translators:
|
||||
|
||||
* Spanish - tzahi12345
|
||||
* German - UnlimitedCookies
|
||||
* Chinese - TyRoyal
|
||||
|
||||
@@ -155,8 +155,8 @@ if (just_restarted) {
|
||||
fs.unlinkSync('restart.json');
|
||||
}
|
||||
|
||||
// updates & starts youtubedl
|
||||
startYoutubeDL();
|
||||
// updates & starts youtubedl (commented out b/c of repo takedown)
|
||||
// startYoutubeDL();
|
||||
|
||||
var validDownloadingAgents = [
|
||||
'aria2c',
|
||||
@@ -558,6 +558,9 @@ async function loadConfig() {
|
||||
// creates archive path if missing
|
||||
await fs.ensureDir(archivePath);
|
||||
|
||||
// now this is done here due to youtube-dl's repo takedown
|
||||
await startYoutubeDL();
|
||||
|
||||
// get subscriptions
|
||||
if (allowSubscriptions) {
|
||||
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
||||
@@ -1376,7 +1379,7 @@ async function downloadFileByURL_normal(url, type, options, sessionID = null) {
|
||||
}
|
||||
|
||||
async function generateArgs(url, type, options) {
|
||||
var videopath = '%(title)s';
|
||||
var videopath = config_api.getConfigItem('ytdl_default_file_output') ? config_api.getConfigItem('ytdl_default_file_output') : '%(title)s';
|
||||
var globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
||||
let useCookies = config_api.getConfigItem('ytdl_use_cookies');
|
||||
var is_audio = type === 'audio';
|
||||
@@ -1592,6 +1595,8 @@ function checkDownloadPercent(download) {
|
||||
const filename = path.format(path.parse(download['_filename'].substring(0, download['_filename'].length-4)));
|
||||
const resulting_file_size = download['filesize'];
|
||||
|
||||
if (!resulting_file_size) return;
|
||||
|
||||
glob(`${filename}*`, (err, files) => {
|
||||
let sum_size = 0;
|
||||
files.forEach(file => {
|
||||
@@ -1613,12 +1618,16 @@ function checkDownloadPercent(download) {
|
||||
|
||||
async function startYoutubeDL() {
|
||||
// auto update youtube-dl
|
||||
if (!debugMode) await autoUpdateYoutubeDL();
|
||||
await autoUpdateYoutubeDL();
|
||||
}
|
||||
|
||||
// auto updates the underlying youtube-dl binary, not YoutubeDL-Material
|
||||
async function autoUpdateYoutubeDL() {
|
||||
return new Promise(resolve => {
|
||||
return new Promise(async resolve => {
|
||||
const default_downloader = config_api.getConfigItem('ytdl_default_downloader');
|
||||
const using_youtube_dlc = default_downloader === 'youtube-dlc';
|
||||
const youtube_dl_tags_url = 'https://api.github.com/repos/ytdl-org/youtube-dl/tags'
|
||||
const youtube_dlc_tags_url = 'https://api.github.com/repos/blackjack4494/yt-dlc/tags'
|
||||
// get current version
|
||||
let current_app_details_path = 'node_modules/youtube-dl/bin/details';
|
||||
let current_app_details_exists = fs.existsSync(current_app_details_path);
|
||||
@@ -1645,42 +1654,77 @@ async function autoUpdateYoutubeDL() {
|
||||
}
|
||||
|
||||
// got version, now let's check the latest version from the youtube-dl API
|
||||
let youtubedl_api_path = 'https://api.github.com/repos/ytdl-org/youtube-dl/tags';
|
||||
let youtubedl_api_path = using_youtube_dlc ? youtube_dlc_tags_url : youtube_dl_tags_url;
|
||||
|
||||
if (default_downloader === 'youtube-dl') {
|
||||
await downloadLatestYoutubeDLBinary('unknown', 'unknown');
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(youtubedl_api_path, {method: 'Get'})
|
||||
.then(async res => res.json())
|
||||
.then(async (json) => {
|
||||
// check if the versions are different
|
||||
if (!json || !json[0]) {
|
||||
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
||||
resolve(false);
|
||||
return false;
|
||||
}
|
||||
const latest_update_version = json[0]['name'];
|
||||
if (current_version !== latest_update_version) {
|
||||
let binary_path = 'node_modules/youtube-dl/bin';
|
||||
// versions different, download new update
|
||||
logger.info('Found new update for youtube-dl. Updating binary...');
|
||||
logger.info(`Found new update for ${default_downloader}. Updating binary...`);
|
||||
try {
|
||||
await checkExistsWithTimeout(stored_binary_path, 10000);
|
||||
} catch(e) {
|
||||
logger.error(`Failed to update youtube-dl - ${e}`);
|
||||
logger.error(`Failed to update ${default_downloader} - ${e}`);
|
||||
}
|
||||
downloader(binary_path, function error(err, done) {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
resolve(false);
|
||||
}
|
||||
logger.info(`Binary successfully updated: ${current_version} -> ${latest_update_version}`);
|
||||
resolve(true);
|
||||
});
|
||||
if (using_youtube_dlc) await downloadLatestYoutubeDLCBinary(latest_update_version);
|
||||
else await downloadLatestYoutubeDLBinary(current_version, latest_update_version);
|
||||
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error('Failed to check youtube-dl version for an update.')
|
||||
logger.error(`Failed to check ${default_downloader} version for an update.`)
|
||||
logger.error(err)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadLatestYoutubeDLBinary(current_version, new_version) {
|
||||
return new Promise(resolve => {
|
||||
let binary_path = 'node_modules/youtube-dl/bin';
|
||||
downloader(binary_path, function error(err, done) {
|
||||
if (err) {
|
||||
logger.error(`youtube-dl failed to update. Restart the server to try again.`);
|
||||
logger.error(err);
|
||||
resolve(false);
|
||||
}
|
||||
logger.info(`youtube-dl successfully updated!`);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadLatestYoutubeDLCBinary(new_version) {
|
||||
const file_ext = is_windows ? '.exe' : '';
|
||||
|
||||
const download_url = `https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc${file_ext}`;
|
||||
const output_path = `node_modules/youtube-dl/bin/youtube-dl${file_ext}`;
|
||||
|
||||
await fetchFile(download_url, output_path, `youtube-dlc ${new_version}`);
|
||||
|
||||
const details_path = 'node_modules/youtube-dl/bin/details';
|
||||
const details_json = fs.readJSONSync('node_modules/youtube-dl/bin/details');
|
||||
details_json['version'] = new_version;
|
||||
|
||||
fs.writeJSONSync(details_path, details_json);
|
||||
}
|
||||
|
||||
async function checkExistsWithTimeout(filePath, timeout) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
|
||||
@@ -1732,7 +1776,7 @@ app.use(function(req, res, next) {
|
||||
next();
|
||||
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
||||
next();
|
||||
} else if (req.path.includes('/api/stream/')) {
|
||||
} else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/')) {
|
||||
next();
|
||||
} else {
|
||||
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
||||
@@ -1887,7 +1931,7 @@ app.get('/api/getMp3s', optionalJwt, async function(req, res) {
|
||||
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
// add thumbnails if present
|
||||
await addThumbnails(mp3s);
|
||||
// await addThumbnails(mp3s);
|
||||
}
|
||||
|
||||
|
||||
@@ -1914,7 +1958,7 @@ app.get('/api/getMp4s', optionalJwt, async function(req, res) {
|
||||
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
// add thumbnails if present
|
||||
await addThumbnails(mp4s);
|
||||
// await addThumbnails(mp4s);
|
||||
}
|
||||
|
||||
res.send({
|
||||
@@ -2005,7 +2049,7 @@ app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
||||
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
// add thumbnails if present
|
||||
await addThumbnails(files);
|
||||
// await addThumbnails(files);
|
||||
}
|
||||
|
||||
res.send({
|
||||
@@ -2152,6 +2196,7 @@ app.post('/api/updateCategories', optionalJwt, async (req, res) => {
|
||||
app.post('/api/subscribe', optionalJwt, async (req, res) => {
|
||||
let name = req.body.name;
|
||||
let url = req.body.url;
|
||||
let maxQuality = req.body.maxQuality;
|
||||
let timerange = req.body.timerange;
|
||||
let streamingOnly = req.body.streamingOnly;
|
||||
let audioOnly = req.body.audioOnly;
|
||||
@@ -2161,6 +2206,7 @@ app.post('/api/subscribe', optionalJwt, async (req, res) => {
|
||||
const new_sub = {
|
||||
name: name,
|
||||
url: url,
|
||||
maxQuality: maxQuality,
|
||||
id: uuid(),
|
||||
streamingOnly: streamingOnly,
|
||||
user_uid: user_uid,
|
||||
@@ -2688,6 +2734,12 @@ app.get('/api/stream/:id', optionalJwt, (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/thumbnail/:path', optionalJwt, async (req, res) => {
|
||||
let file_path = decodeURIComponent(req.params.path);
|
||||
if (fs.existsSync(file_path)) path.isAbsolute(file_path) ? res.sendFile(file_path) : res.sendFile(path.join(__dirname, file_path));
|
||||
else res.sendStatus(404);
|
||||
});
|
||||
|
||||
// Downloads management
|
||||
|
||||
app.get('/api/downloads', async (req, res) => {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"default_file_output": "",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false,
|
||||
@@ -49,6 +50,7 @@
|
||||
}
|
||||
},
|
||||
"Advanced": {
|
||||
"default_downloader": "youtube-dl",
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"multi_user_mode": false,
|
||||
|
||||
@@ -184,6 +184,7 @@ DEFAULT_CONFIG = {
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/",
|
||||
"default_file_output": "",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false,
|
||||
@@ -226,6 +227,7 @@ DEFAULT_CONFIG = {
|
||||
}
|
||||
},
|
||||
"Advanced": {
|
||||
"default_downloader": "youtube-dl",
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"multi_user_mode": false,
|
||||
|
||||
@@ -18,6 +18,10 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_video_folder_path',
|
||||
'path': 'YoutubeDLMaterial.Downloader.path-video'
|
||||
},
|
||||
'ytdl_default_file_output': {
|
||||
'key': 'ytdl_default_file_output',
|
||||
'path': 'YoutubeDLMaterial.Downloader.default_file_output'
|
||||
},
|
||||
'ytdl_use_youtubedl_archive': {
|
||||
'key': 'ytdl_use_youtubedl_archive',
|
||||
'path': 'YoutubeDLMaterial.Downloader.use_youtubedl_archive'
|
||||
@@ -130,6 +134,10 @@ let CONFIG_ITEMS = {
|
||||
},
|
||||
|
||||
// Advanced
|
||||
'ytdl_default_downloader': {
|
||||
'key': 'ytdl_default_downloader',
|
||||
'path': 'YoutubeDLMaterial.Advanced.default_downloader'
|
||||
},
|
||||
'ytdl_use_default_downloading_agent': {
|
||||
'key': 'ytdl_use_default_downloading_agent',
|
||||
'path': 'YoutubeDLMaterial.Advanced.use_default_downloading_agent'
|
||||
|
||||
@@ -114,7 +114,11 @@ async function getSubscriptionInfo(sub, user_uid = null) {
|
||||
continue;
|
||||
}
|
||||
if (!sub.name) {
|
||||
sub.name = sub.isPlaylist ? output_json.playlist_title : output_json.uploader;
|
||||
if (sub.isPlaylist) {
|
||||
sub.name = output_json.playlist_title ? output_json.playlist_title : output_json.playlist;
|
||||
} else {
|
||||
sub.name = output_json.uploader;
|
||||
}
|
||||
// if it's now valid, update
|
||||
if (sub.name) {
|
||||
if (user_uid)
|
||||
@@ -296,7 +300,8 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
qualityPath.push('-x');
|
||||
qualityPath.push('--audio-format', 'mp3');
|
||||
} else {
|
||||
qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4']
|
||||
if (!sub.maxQuality || sub.maxQuality === 'best') qualityPath = ['-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4'];
|
||||
else qualityPath = ['-f', `bestvideo[height<=${sub.maxQuality}]+bestaudio/best[height<=${sub.maxQuality}]`, '--merge-output-format', 'mp4'];
|
||||
}
|
||||
|
||||
downloadConfig.push(...qualityPath)
|
||||
@@ -351,7 +356,7 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||
logger.verbose('Subscription: finished check for ' + sub.name);
|
||||
if (err && !output) {
|
||||
logger.error(err.stderr);
|
||||
logger.error(err.stderr ? err.stderr : err.message);
|
||||
if (err.stderr.includes('This video is unavailable')) {
|
||||
logger.info('An error was encountered with at least one video, backup method will be used.')
|
||||
try {
|
||||
|
||||
@@ -114,6 +114,7 @@ function getExpectedFileSize(info_json) {
|
||||
const formats = info_json['format_id'].split('+');
|
||||
let expected_filesize = 0;
|
||||
formats.forEach(format_id => {
|
||||
if (!info_json.formats) return expected_filesize;
|
||||
info_json.formats.forEach(available_format => {
|
||||
if (available_format.format_id === format_id && available_format.filesize) {
|
||||
expected_filesize += available_format.filesize;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="row justify-content-center">
|
||||
<ng-container *ngIf="normal_files_received">
|
||||
<div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
|
||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)"></app-unified-file-card>
|
||||
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? '?jwt=' + this.postsService.token : ''"></app-unified-file-card>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<div style="padding:5px">
|
||||
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
|
||||
<div style="position: relative">
|
||||
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailBlob ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
|
||||
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailPath ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
|
||||
<div class="duration-time">
|
||||
{{file_length}}
|
||||
</div>
|
||||
|
||||
@@ -44,11 +44,13 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
@Input() is_playlist = false;
|
||||
@Input() index: number;
|
||||
@Input() locale = null;
|
||||
@Input() baseStreamPath = null;
|
||||
@Input() jwtString = null;
|
||||
@Output() goToFile = new EventEmitter<any>();
|
||||
@Output() goToSubscription = new EventEmitter<any>();
|
||||
@Output() deleteFile = new EventEmitter<any>();
|
||||
@Output() editPlaylist = new EventEmitter<any>();
|
||||
|
||||
|
||||
|
||||
@ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger;
|
||||
contextMenuPosition = { x: '0px', y: '0px' };
|
||||
@@ -67,11 +69,12 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
this.file_length = fancyTimeFormat(this.file_obj.duration);
|
||||
}
|
||||
|
||||
if (this.file_obj && this.file_obj.thumbnailBlob) {
|
||||
const mime = getMimeByFilename(this.file_obj.thumbnailPath);
|
||||
if (this.file_obj && this.file_obj.thumbnailPath) {
|
||||
this.thumbnailBlobURL = `${this.baseStreamPath}thumbnail/${encodeURIComponent(this.file_obj.thumbnailPath)}${this.jwtString}`;
|
||||
/*const mime = getMimeByFilename(this.file_obj.thumbnailPath);
|
||||
const blob = new Blob([new Uint8Array(this.file_obj.thumbnailBlob.data)], {type: mime});
|
||||
const bloburl = URL.createObjectURL(blob);
|
||||
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);
|
||||
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
<mat-checkbox [disabled]="true" [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 mt-2">
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="Max quality" i18n-placeholder="Max quality placeholder" [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.maxQuality">
|
||||
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<mat-checkbox [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.streamingOnly"><ng-container i18n="Streaming-only mode">Streaming-only mode</ng-container></mat-checkbox>
|
||||
|
||||
@@ -22,6 +22,36 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
||||
audioOnlyMode = null;
|
||||
download_all = null;
|
||||
|
||||
available_qualities = [
|
||||
{
|
||||
'label': 'Best',
|
||||
'value': 'best'
|
||||
},
|
||||
{
|
||||
'label': '4K',
|
||||
'value': '2160'
|
||||
},
|
||||
{
|
||||
'label': '1440p',
|
||||
'value': '1440'
|
||||
},
|
||||
{
|
||||
'label': '1080p',
|
||||
'value': '1080'
|
||||
},
|
||||
{
|
||||
'label': '720p',
|
||||
'value': '720'
|
||||
},
|
||||
{
|
||||
'label': '480p',
|
||||
'value': '480'
|
||||
},
|
||||
{
|
||||
'label': '360p',
|
||||
'value': '360'
|
||||
}
|
||||
];
|
||||
|
||||
time_units = [
|
||||
'day',
|
||||
@@ -39,16 +69,12 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
||||
|
||||
if (this.sub.timerange) {
|
||||
const timerange_str = this.sub.timerange.split('-')[1];
|
||||
console.log(timerange_str);
|
||||
const number = timerange_str.replace(/\D/g,'');
|
||||
let units = timerange_str.replace(/[0-9]/g, '');
|
||||
|
||||
console.log(units);
|
||||
|
||||
// // remove plural on units
|
||||
// if (units[units.length-1] === 's') {
|
||||
// units = units.substring(0, units.length-1);
|
||||
// }
|
||||
if (+number === 1) {
|
||||
units = units.replace('s', '');
|
||||
}
|
||||
|
||||
this.timerange_amount = parseInt(number);
|
||||
this.timerange_unit = units;
|
||||
@@ -71,9 +97,10 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
saveSubscription() {
|
||||
this.postsService.updateSubscription(this.sub).subscribe(res => {
|
||||
this.postsService.updateSubscription(this.new_sub).subscribe(res => {
|
||||
this.sub = this.new_sub;
|
||||
this.new_sub = JSON.parse(JSON.stringify(this.sub));
|
||||
this.postsService.reloadSubscriptions();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,12 +112,16 @@ export class EditSubscriptionDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
timerangeChanged(value, select_changed) {
|
||||
console.log(this.timerange_amount);
|
||||
console.log(this.timerange_unit);
|
||||
if (+this.timerange_amount === 1) {
|
||||
this.timerange_unit = this.timerange_unit.replace('s', '');
|
||||
} else {
|
||||
if (!this.timerange_unit.includes('s')) {
|
||||
this.timerange_unit += 's';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.timerange_amount && this.timerange_unit && !this.download_all) {
|
||||
this.new_sub.timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
|
||||
console.log(this.new_sub.timerange);
|
||||
} else {
|
||||
this.new_sub.timerange = null;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,13 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-2">
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="Max quality" i18n-placeholder="Max quality placeholder" [disabled]="audioOnlyMode" [(ngModel)]="maxQuality">
|
||||
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<mat-checkbox [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
|
||||
|
||||
@@ -17,6 +17,8 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
url = null;
|
||||
name = null;
|
||||
|
||||
maxQuality = 'best';
|
||||
|
||||
// state
|
||||
subscribing = false;
|
||||
|
||||
@@ -29,12 +31,43 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
customFileOutput = '';
|
||||
customArgs = '';
|
||||
|
||||
available_qualities = [
|
||||
{
|
||||
'label': 'Best',
|
||||
'value': 'best'
|
||||
},
|
||||
{
|
||||
'label': '4K',
|
||||
'value': '2160'
|
||||
},
|
||||
{
|
||||
'label': '1440p',
|
||||
'value': '1440'
|
||||
},
|
||||
{
|
||||
'label': '1080p',
|
||||
'value': '1080'
|
||||
},
|
||||
{
|
||||
'label': '720p',
|
||||
'value': '720'
|
||||
},
|
||||
{
|
||||
'label': '480p',
|
||||
'value': '480'
|
||||
},
|
||||
{
|
||||
'label': '360p',
|
||||
'value': '360'
|
||||
}
|
||||
];
|
||||
|
||||
time_units = [
|
||||
'day',
|
||||
'week',
|
||||
'month',
|
||||
'year'
|
||||
]
|
||||
];
|
||||
|
||||
constructor(private postsService: PostsService,
|
||||
private snackBar: MatSnackBar,
|
||||
@@ -57,7 +90,7 @@ export class SubscribeDialogComponent implements OnInit {
|
||||
if (!this.download_all) {
|
||||
timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
|
||||
}
|
||||
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode,
|
||||
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode, this.maxQuality,
|
||||
this.audioOnlyMode, this.customArgs, this.customFileOutput).subscribe(res => {
|
||||
this.subscribing = false;
|
||||
if (res['new_sub']) {
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
|
||||
<app-recent-videos></app-recent-videos>
|
||||
<app-recent-videos #recentVideos></app-recent-videos>
|
||||
<br/>
|
||||
<h4 style="text-align: center">Custom playlists</h4>
|
||||
<app-custom-playlists></app-custom-playlists>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.com
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component';
|
||||
import { RecentVideosComponent } from 'app/components/recent-videos/recent-videos.component';
|
||||
|
||||
export let audioFilesMouseHovering = false;
|
||||
export let videoFilesMouseHovering = false;
|
||||
@@ -200,6 +201,7 @@ export class MainComponent implements OnInit {
|
||||
formats_loading = false;
|
||||
|
||||
@ViewChild('urlinput', { read: ElementRef }) urlInput: ElementRef;
|
||||
@ViewChild('recentVideos') recentVideos: RecentVideosComponent;
|
||||
@ViewChildren('audiofilecard') audioFileCards: QueryList<FileCardComponent>;
|
||||
@ViewChildren('videofilecard') videoFileCards: QueryList<FileCardComponent>;
|
||||
last_valid_url = '';
|
||||
@@ -487,6 +489,7 @@ export class MainComponent implements OnInit {
|
||||
this.downloadingfile = false;
|
||||
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
|
||||
// do nothing
|
||||
this.reloadRecentVideos();
|
||||
} else {
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
|
||||
@@ -496,6 +499,7 @@ export class MainComponent implements OnInit {
|
||||
} else {
|
||||
this.downloadAudioFile(decodeURI(name));
|
||||
}
|
||||
this.reloadRecentVideos();
|
||||
} else {
|
||||
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
|
||||
if (is_playlist) {
|
||||
@@ -524,6 +528,7 @@ export class MainComponent implements OnInit {
|
||||
this.downloadingfile = false;
|
||||
if (this.multiDownloadMode && !this.downloadOnlyMode && !navigate_mode) {
|
||||
// do nothing
|
||||
this.reloadRecentVideos();
|
||||
} else {
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (forceView === false && this.downloadOnlyMode) {
|
||||
@@ -533,6 +538,7 @@ export class MainComponent implements OnInit {
|
||||
} else {
|
||||
this.downloadVideoFile(decodeURI(name));
|
||||
}
|
||||
this.reloadRecentVideos();
|
||||
} else {
|
||||
localStorage.setItem('player_navigator', this.router.url.split(';')[0]);
|
||||
if (is_playlist) {
|
||||
@@ -1161,4 +1167,10 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reloadRecentVideos() {
|
||||
if (this.recentVideos) {
|
||||
this.recentVideos.getAllFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { v4 as uuid } from 'uuid';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import * as Fingerprint2 from 'fingerprintjs2';
|
||||
import { isoLangs } from './settings/locales_list';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
@Injectable()
|
||||
export class PostsService implements CanActivate {
|
||||
@@ -58,7 +59,7 @@ export class PostsService implements CanActivate {
|
||||
locale = isoLangs['en'];
|
||||
|
||||
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
|
||||
public snackBar: MatSnackBar) {
|
||||
public snackBar: MatSnackBar, private titleService: Title) {
|
||||
console.log('PostsService Initialized...');
|
||||
this.path = this.document.location.origin + '/api/';
|
||||
|
||||
@@ -88,6 +89,7 @@ export class PostsService implements CanActivate {
|
||||
const result = !this.debugMode ? res['config_file'] : res;
|
||||
if (result) {
|
||||
this.config = result['YoutubeDLMaterial'];
|
||||
this.titleService.setTitle(this.config['Extra']['title_top']);
|
||||
if (this.config['Advanced']['multi_user_mode']) {
|
||||
this.checkAdminCreationStatus();
|
||||
// login stuff
|
||||
@@ -335,9 +337,11 @@ export class PostsService implements CanActivate {
|
||||
});
|
||||
}
|
||||
|
||||
createSubscription(url, name, timerange = null, streamingOnly = false, audioOnly = false, customArgs = null, customFileOutput = null) {
|
||||
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, streamingOnly: streamingOnly,
|
||||
audioOnly: audioOnly, customArgs: customArgs, customFileOutput: customFileOutput}, this.httpOptions);
|
||||
createSubscription(url, name, timerange = null, streamingOnly = false, maxQuality = 'best', audioOnly = false,
|
||||
customArgs = null, customFileOutput = null) {
|
||||
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, maxQuality: maxQuality,
|
||||
streamingOnly: streamingOnly, audioOnly: audioOnly, customArgs: customArgs,
|
||||
customFileOutput: customFileOutput}, this.httpOptions);
|
||||
}
|
||||
|
||||
updateSubscription(subscription) {
|
||||
|
||||
@@ -115,9 +115,19 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<mat-form-field class="text-field" color="accent">
|
||||
<input matInput [(ngModel)]="new_config['Downloader']['default_file_output']" matInput placeholder="Default file output" i18n-placeholder="Default file output placeholder">
|
||||
<mat-hint><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
|
||||
<ng-container i18n="Youtube-dl output template documentation link">Documentation</ng-container></a>.
|
||||
<ng-container i18n="Custom Output input hint">Path is relative to the above download paths. Don't include extension.</ng-container>
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4 mb-5">
|
||||
<mat-form-field class="text-field" style="margin-right: 12px;" color="accent">
|
||||
<textarea matInput [(ngModel)]="new_config['Downloader']['custom_args']" placeholder="Custom args" i18n-placeholder="Custom args input placeholder"></textarea>
|
||||
<textarea matInput [(ngModel)]="new_config['Downloader']['custom_args']" placeholder="Global custom args" i18n-placeholder="Custom args input placeholder"></textarea>
|
||||
<mat-hint><ng-container i18n="Custom args setting input hint">Global custom args for downloads on the home page. Args are delimited using two commas like so: ,,</ng-container></mat-hint>
|
||||
<button class="args-edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
|
||||
</mat-form-field>
|
||||
@@ -258,11 +268,20 @@
|
||||
<div *ngIf="new_config" class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 mt-3">
|
||||
<mat-form-field>
|
||||
<mat-label><ng-container i18n="Default downloader select label">Select a downloader</ng-container></mat-label>
|
||||
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['default_downloader']">
|
||||
<mat-option value="youtube-dlc">youtube-dlc</mat-option>
|
||||
<mat-option value="youtube-dl">youtube-dl</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-1">
|
||||
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['use_default_downloading_agent']"><ng-container i18n="Use default downloading agent setting">Use default downloading agent</ng-container></mat-checkbox>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<mat-form-field>
|
||||
<mat-label><ng-container i18n="Custom downloader select label">Select a downloader</ng-container></mat-label>
|
||||
<mat-label><ng-container i18n="Custom downloader select label">Select a download agent</ng-container></mat-label>
|
||||
<mat-select [disabled]="new_config['Advanced']['use_default_downloading_agent']" color="accent" [(ngModel)]="new_config['Advanced']['custom_downloading_agent']">
|
||||
<mat-option value="aria2c">aria2c</mat-option>
|
||||
<mat-option value="avconv">avconv</mat-option>
|
||||
@@ -274,7 +293,7 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-1">
|
||||
<div class="col-12 mt-2">
|
||||
<mat-form-field>
|
||||
<mat-label><ng-container i18n="Log Level label">Log Level</ng-container></mat-label>
|
||||
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['logger_level']">
|
||||
@@ -286,7 +305,7 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 mt-2 mb-1">
|
||||
<div class="col-12 mb-1">
|
||||
<mat-form-field>
|
||||
<mat-label><ng-container i18n="Login expiration select label">Login expiration</ng-container></mat-label>
|
||||
<mat-select color="accent" [(ngModel)]="new_config['Advanced']['jwt_expiration']">
|
||||
|
||||
@@ -164,7 +164,7 @@ export class SubscriptionComponent implements OnInit {
|
||||
editSubscription() {
|
||||
this.dialog.open(EditSubscriptionDialogComponent, {
|
||||
data: {
|
||||
sub: this.subscription
|
||||
sub: this.postsService.getSubscriptionByID(this.subscription.id)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
16
src/app/youtube.service.spec.ts
Normal file
16
src/app/youtube.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { YoutubeService } from './youtube.service';
|
||||
|
||||
describe('YoutubeService', () => {
|
||||
let service: YoutubeService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(YoutubeService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
116
src/app/youtube.service.ts
Normal file
116
src/app/youtube.service.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export class Result {
|
||||
id: string
|
||||
title: string
|
||||
desc: string
|
||||
thumbnailUrl: string
|
||||
videoUrl: string
|
||||
uploaded: any;
|
||||
|
||||
constructor(obj?: any) {
|
||||
this.id = obj && obj.id || null
|
||||
this.title = obj && obj.title || null
|
||||
this.desc = obj && obj.desc || null
|
||||
this.thumbnailUrl = obj && obj.thumbnailUrl || null
|
||||
this.uploaded = obj && obj.uploaded || null
|
||||
this.videoUrl = obj && obj.videoUrl || `https://www.youtube.com/watch?v=${this.id}`
|
||||
|
||||
this.uploaded = formatDate(Date.parse(this.uploaded));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class YoutubeService {
|
||||
|
||||
base_url = 'https://www.googleapis.com/youtube/v3/';
|
||||
key = null;
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
initializeAPI(key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
search(query: string): Observable<Result[]> {
|
||||
const url_sub_path = 'search';
|
||||
|
||||
if (this.ValidURL(query)) {
|
||||
return new Observable<Result[]>();
|
||||
}
|
||||
const params: string = [
|
||||
`q=${query}`,
|
||||
`key=${this.key}`,
|
||||
`part=snippet`,
|
||||
`type=video`,
|
||||
`maxResults=5`
|
||||
].join('&')
|
||||
const queryUrl = `${this.url}?${params}`
|
||||
return this.http.get(queryUrl).map(response => {
|
||||
return <any>response['items'].map(item => {
|
||||
return new Result({
|
||||
id: item.id.videoId,
|
||||
title: item.snippet.title,
|
||||
desc: item.snippet.description,
|
||||
thumbnailUrl: item.snippet.thumbnails.high.url,
|
||||
uploaded: item.snippet.publishedAt
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getSubscribedChannels() {
|
||||
const url_sub_path = ''
|
||||
// on the first iteration, don't use a token. but because of the 50 channels limit, you need to use the returned token
|
||||
// to retrieve the next list of 50 channels until a next token is not given
|
||||
// https://stackoverflow.com/questions/52803732/youtube-api-v3-maximum-number-of-videos-only-50
|
||||
// https://developers.google.com/youtube/v3/docs/subscriptions/list?apix_params=%7B%22part%22%3A%5B%22snippet%2CcontentDetails%22%5D%2C%22maxResults%22%3A50%2C%22mine%22%3Atrue%2C%22pageToken%22%3A%22CGQQAA%22%7D
|
||||
}
|
||||
|
||||
getSubscribedChannelsWithToken() {
|
||||
|
||||
}
|
||||
|
||||
// checks if url is a valid URL
|
||||
ValidURL(str) {
|
||||
// tslint:disable-next-line: max-line-length
|
||||
const strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
|
||||
const re = new RegExp(strRegex);
|
||||
return re.test(str);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateVal) {
|
||||
const newDate = new Date(dateVal);
|
||||
|
||||
const sMonth = padValue(newDate.getMonth() + 1);
|
||||
const sDay = padValue(newDate.getDate());
|
||||
const sYear = newDate.getFullYear();
|
||||
let sHour: any;
|
||||
sHour = newDate.getHours();
|
||||
const sMinute = padValue(newDate.getMinutes());
|
||||
let sAMPM = 'AM';
|
||||
|
||||
const iHourCheck = parseInt(sHour, 10);
|
||||
|
||||
if (iHourCheck > 12) {
|
||||
sAMPM = 'PM';
|
||||
sHour = iHourCheck - 12;
|
||||
} else if (iHourCheck === 0) {
|
||||
sHour = '12';
|
||||
}
|
||||
|
||||
sHour = padValue(sHour);
|
||||
|
||||
return sMonth + '-' + sDay + '-' + sYear + ' ' + sHour + ':' + sMinute + ' ' + sAMPM;
|
||||
}
|
||||
|
||||
function padValue(value) {
|
||||
return (value < 10) ? '0' + value : value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user