Skip to content

Commit 1707ff0

Browse files
author
Leszek Wiesner
authored
Merge pull request #350 from Lezek123/proxy-setup
Improved proxy setup
2 parents 5371fa9 + 78e0d76 commit 1707ff0

File tree

23 files changed

+590
-195
lines changed

23 files changed

+590
-195
lines changed

Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ COPY package.json package-lock.json ./
2828
# Install dependencies
2929
RUN npm install
3030

31+
# Force-update yt-dlp
32+
RUN node ./node_modules/youtube-dl-exec/scripts/postinstall.js
33+
3134
# Copy the rest of your application
3235
COPY . .
3336

config.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,13 @@ youtube:
4646
# maxAllowedQuotaUsageInPercentage: 95
4747
# proxy:
4848
# urls:
49-
# ["socks5://proxy1:1080", "socks5://proxy2:1080"]
49+
# - socks5://proxy1:1080
50+
# - socks5://proxy2:1080
51+
# exclusionDuration: 14400
52+
# waitInterval: 60
53+
# chain:
54+
# url: socks5://proxy.chain:1080
55+
# configDir: /etc
5056
aws:
5157
endpoint: http://localhost:4566
5258
region: us-east-1

docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ services:
6161
# - /var/run/docker.sock:/var/run/docker.sock
6262
- ./local/logs:/youtube-synch/local/logs
6363
- ./local/data:/youtube-synch/local/data
64-
- ./proxychains4.conf:/etc/proxychains4.conf
6564
# mount Google Cloud's Application Default Credentials file. A default bind mount is created
6665
# as workaround for scenario when `YT_SYNCH__YOUTUBE__ADC_KEY_FILE_PATH` will be undefined,
6766
# since docker-compose does not support creating bind-mount volume with empty path.

package-lock.json

Lines changed: 9 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
"reflect-metadata": "^0.1.13",
141141
"rxjs": "^7.5.2",
142142
"sleep-promise": "^9.1.0",
143+
"socks-proxy-agent": "^8.0.5",
143144
"swagger-ui-express": "^4.3.0",
144145
"uuid": "^9.0.0",
145146
"winston": "^3.8.2",

proxychains4.conf

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/app/index.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import path from 'path'
12
import fs from 'fs'
23
import _ from 'lodash'
34
import nodeCleanup from 'node-cleanup'
@@ -12,6 +13,8 @@ import { ContentProcessingClient, ContentProcessingService } from '../services/s
1213
import { YoutubePollingService } from '../services/syncProcessing/YoutubePollingService'
1314
import { IYoutubeApi, YoutubeApi } from '../services/youtube/api'
1415
import { Config, DisplaySafeConfig } from '../types'
16+
import { Socks5ProxyService } from '../services/proxy/Socks5ProxyService'
17+
import { THUMBNAILS_SUBDIR } from '../services/syncProcessing/ContentDownloadService'
1518

1619
export class Service {
1720
private config: Config
@@ -25,15 +28,17 @@ export class Service {
2528
private youtubePollingService: YoutubePollingService
2629
private contentProcessingService: ContentProcessingService
2730
private contentProcessingClient: ContentProcessingClient
31+
private socks5ProxyService?: Socks5ProxyService
2832
private isStopping = false
2933

3034
constructor(config: Config) {
3135
this.config = config
3236
this.logging = LoggingService.withAppConfig(config.logs)
3337
this.logger = this.logging.createLogger('Server')
38+
this.socks5ProxyService = config.proxy ? new Socks5ProxyService(config.proxy, this.logging) : undefined
3439
this.queryNodeApi = new QueryNodeApi(config.endpoints.queryNode, this.logging)
3540
this.dynamodbService = new DynamodbService(this.config.aws)
36-
this.youtubeApi = YoutubeApi.create(this.config, this.dynamodbService.repo.stats, this.logging)
41+
this.youtubeApi = YoutubeApi.create(this.config, this.dynamodbService.repo.stats, this.logging, this.socks5ProxyService)
3742
this.runtimeApi = new RuntimeApi(config.endpoints.joystreamNodeWs, this.logging)
3843
this.joystreamClient = new JoystreamClient(config, this.runtimeApi, this.queryNodeApi, this.logging)
3944
this.contentProcessingClient = new ContentProcessingClient({ ...config.sync, ...config.endpoints })
@@ -69,10 +74,15 @@ export class Service {
6974
private checkConfigDirectories(): void {
7075
if (this.config.sync.enable) {
7176
this.checkConfigDir('sync.downloadsDir', this.config.sync.downloadsDir)
77+
const thumbsDir = path.join(this.config.sync.downloadsDir, THUMBNAILS_SUBDIR)
78+
this.checkConfigDir('thumbnails', thumbsDir)
7279
}
7380
if (this.config.logs?.file) {
7481
this.checkConfigDir('logs.file.path', this.config.logs.file.path)
7582
}
83+
if (this.config.proxy?.chain) {
84+
this.checkConfigDir('proxy.chain.configDir', this.config.proxy.chain.configDir)
85+
}
7686
}
7787

7888
private async startSync(): Promise<void> {
@@ -84,7 +94,8 @@ export class Service {
8494
this.youtubeApi,
8595
this.runtimeApi,
8696
this.joystreamClient,
87-
this.queryNodeApi
97+
this.queryNodeApi,
98+
this.socks5ProxyService
8899
)
89100

90101
const {

src/cli/commands/downloadVideo.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { flags } from '@oclif/command'
2+
import fs from 'fs'
3+
import path from 'path'
4+
import DefaultCommandBase from '../base/default'
5+
import { LoggingService } from '../../services/logging'
6+
import { YoutubeClient } from '../../services/youtube/api'
7+
import { Socks5ProxyService } from '../../services/proxy/Socks5ProxyService'
8+
import assert from 'assert'
9+
10+
export default class DownloadVideo extends DefaultCommandBase {
11+
static description = 'Perform a test video download.'
12+
13+
static flags = {
14+
url: flags.string({
15+
multiple: true,
16+
description: 'YouTube video url(s) of the video(s) to be downloaded'
17+
}),
18+
skipProxy: flags.boolean({
19+
description: 'Skip proxy even if configured.',
20+
default: false,
21+
}),
22+
...DefaultCommandBase.flags,
23+
}
24+
25+
async run(): Promise<void> {
26+
const { url: videoUrls, skipProxy } = this.parse(DownloadVideo).flags
27+
28+
const { downloadsDir } = this.appConfig.sync
29+
assert(downloadsDir, "Download dir needs to be specified!")
30+
31+
const logging = LoggingService.withCLIConfig()
32+
let proxyConfig = this.appConfig.proxy
33+
if (skipProxy) {
34+
proxyConfig = undefined
35+
this.log('Skipping proxy setup...')
36+
} else {
37+
this.log('Using proxy configuration:', proxyConfig)
38+
}
39+
const proxyService = proxyConfig ? new Socks5ProxyService(proxyConfig, logging) : undefined
40+
const youtubeClient = new YoutubeClient(this.appConfig, logging, proxyService)
41+
for (const videoUrl of videoUrls) {
42+
const proxy = await proxyService?.getProxy(videoUrl)
43+
if (proxy) {
44+
this.log(`Selected proxy: ${proxy}`)
45+
}
46+
const checkResp = await youtubeClient.checkVideo(videoUrl, proxy)
47+
this.log(`Downloading ${videoUrl}...`)
48+
this.log(`Selected format: ${checkResp.format}`)
49+
this.log(`Expected size: ${checkResp.filesize_approx}`)
50+
const downloadResp = await youtubeClient.downloadVideo(videoUrl, downloadsDir, proxy)
51+
assert(
52+
fs.existsSync(path.join(downloadsDir, `${downloadResp.id}.${downloadResp.ext}`)),
53+
'Download error: File not found'
54+
)
55+
proxyService?.unbindProxy(downloadResp.id)
56+
}
57+
this.log("All videos successfully downloaded!")
58+
}
59+
60+
async finally(): Promise<void> {
61+
/* Do nothing */
62+
}
63+
}

src/schemas/config.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,17 +228,41 @@ export const configSchema: JSONSchema7 = objectSchema({
228228
title: 'Socks5 proxy client configuration used by yt-dlp to bypass IP blockage by Youtube',
229229
description: 'Socks5 proxy client configuration used by yt-dlp to bypass IP blockage by Youtube',
230230
properties: {
231+
chain: objectSchema({
232+
title: 'Socks5 proxy chaining configuration',
233+
properties: {
234+
url: {
235+
description: 'Url of the socks5 proxy that all other proxy requests will be chained through.',
236+
type: 'string',
237+
pattern: '^socks5://'
238+
},
239+
configDir: {
240+
description: 'Absolute path to a location where proxychains4.conf file will be stored',
241+
type: 'string'
242+
},
243+
},
244+
required: ['url', 'configDir']
245+
}),
231246
urls: {
232247
description: 'List of available socks5 proxy URLs',
233248
type: 'array',
234249
items: {
235-
description: 'Socks5 proxy url, e.g. ["socks://localhost:1080"]',
250+
description: 'Socks5 proxy url, e.g. ["socks5://localhost:1080"]',
236251
type: 'string',
252+
pattern: '^socks5://'
237253
},
238254
minItems: 1,
255+
},
256+
waitInterval: {
257+
description: 'How long should the application wait in case no proxies are available (in seconds)',
258+
type: 'integer',
259+
},
260+
exclusionDuration: {
261+
description: `How long should the proxy remain excluded in case it's blocked (in seconds)`,
262+
type: 'integer',
239263
}
240264
},
241-
required: [],
265+
required: ['urls', 'exclusionDuration', 'waitInterval'],
242266
}),
243267

244268
creatorOnboardingRequirements: objectSchema({

src/scripts/downloadAsset.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import axios, { AddressFamily, AxiosRequestConfig } from 'axios'
2+
import assert from 'assert'
3+
import { argv } from 'process'
4+
import { SocksProxyAgent } from 'socks-proxy-agent'
5+
import { Readable, promises as streamPromise } from 'stream'
6+
import path from 'path'
7+
import { createWriteStream } from 'fs'
8+
import dns from 'dns'
9+
10+
const isTS = path.extname(__filename) === '.ts'
11+
export const SCRIPT_PATH = __filename.split(path.sep)
12+
.map((part) => part === 'src' ? 'lib' : part)
13+
.join(path.sep)
14+
.replace('.ts', '.js')
15+
16+
function validateArgs() {
17+
try {
18+
const assetUrl = new URL(argv[2])
19+
assert(assetUrl.protocol === 'http:' || assetUrl.protocol === 'https:', `Invalid assetUrl protocol: ${assetUrl.protocol}`)
20+
21+
const destPath = argv[3]
22+
if (!path.isAbsolute(destPath) || !path.extname(destPath)) {
23+
throw new Error('destPath must be an absolute path to a file!')
24+
}
25+
26+
let proxyUrl: URL | undefined = undefined
27+
if (argv[4]) {
28+
proxyUrl = new URL(argv[4])
29+
assert(proxyUrl.protocol === 'socks5:', `Invalid proxyUrl protocol: ${proxyUrl.protocol}`)
30+
}
31+
32+
return { assetUrl: assetUrl.toString(), destPath, proxyUrl: proxyUrl?.toString() }
33+
} catch (e: any) {
34+
throw new Error(`Invalid arguments: ${e.toString()}`)
35+
}
36+
}
37+
38+
// Define a custom lookup function that forces IPv4 resolution
39+
const ipv4Lookup: AxiosRequestConfig<any>['lookup'] = (hostname, options, callback) => {
40+
return dns.lookup(hostname, { ...options, family: 4 }, (err, address, family) => callback(err, address, family as AddressFamily));
41+
}
42+
43+
async function main() {
44+
const { assetUrl, destPath, proxyUrl } = validateArgs()
45+
const reqConfig: AxiosRequestConfig<any> = {
46+
responseType: 'stream',
47+
family: 4,
48+
lookup: ipv4Lookup
49+
}
50+
if (proxyUrl) {
51+
const proxyAgent = new SocksProxyAgent(proxyUrl)
52+
reqConfig.httpAgent = proxyAgent
53+
reqConfig.httpsAgent = proxyAgent
54+
}
55+
const response = await axios.get<Readable>(assetUrl, reqConfig)
56+
const writeStream = createWriteStream(destPath)
57+
await streamPromise.pipeline([response.data, writeStream])
58+
}
59+
60+
// Execute script only if the file was executed directly (not imported as module)
61+
if (require.main === module && !isTS) {
62+
main()
63+
.then(() => process.exit(0))
64+
.catch(e => {
65+
console.error(e)
66+
process.exit(1)
67+
})
68+
}

0 commit comments

Comments
 (0)