mirror of
https://github.com/alexta69/metube.git
synced 2026-06-17 00:30:07 +00:00
some fixes in cookie upload functionality
This commit is contained in:
@@ -89,21 +89,13 @@ The project's Wiki contains examples of useful configurations contributed by use
|
|||||||
|
|
||||||
In case you need to use your browser's cookies with MeTube, for example to download restricted or private videos:
|
In case you need to use your browser's cookies with MeTube, for example to download restricted or private videos:
|
||||||
|
|
||||||
* Add the following to your docker-compose.yml:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
volumes:
|
|
||||||
- /path/to/cookies:/cookies
|
|
||||||
environment:
|
|
||||||
- YTDL_OPTIONS={"cookiefile":"/cookies/cookies.txt"}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Install in your browser an extension to extract cookies:
|
* Install in your browser an extension to extract cookies:
|
||||||
* [Firefox](https://addons.mozilla.org/en-US/firefox/addon/export-cookies-txt/)
|
* [Firefox](https://addons.mozilla.org/en-US/firefox/addon/export-cookies-txt/)
|
||||||
* [Chrome](https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc)
|
* [Chrome](https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc)
|
||||||
* Extract the cookies you need with the extension and rename the file `cookies.txt`
|
* Extract the cookies you need with the extension and save/export them as `cookies.txt`.
|
||||||
* Drop the file in the folder you configured in the docker-compose.yml above
|
* In MeTube, open **Advanced Options** and use the **Upload Cookies** button to upload the file.
|
||||||
* Restart the container
|
* After upload, the cookie indicator should show as active.
|
||||||
|
* Use **Delete Cookies** in the same section to remove uploaded cookies.
|
||||||
|
|
||||||
## 🔌 Browser extensions
|
## 🔌 Browser extensions
|
||||||
|
|
||||||
|
|||||||
+43
-6
@@ -97,10 +97,23 @@ class Config:
|
|||||||
if self.YTDL_OPTIONS_FILE and self.YTDL_OPTIONS_FILE.startswith('.'):
|
if self.YTDL_OPTIONS_FILE and self.YTDL_OPTIONS_FILE.startswith('.'):
|
||||||
self.YTDL_OPTIONS_FILE = str(Path(self.YTDL_OPTIONS_FILE).resolve())
|
self.YTDL_OPTIONS_FILE = str(Path(self.YTDL_OPTIONS_FILE).resolve())
|
||||||
|
|
||||||
|
self._runtime_overrides = {}
|
||||||
|
|
||||||
success,_ = self.load_ytdl_options()
|
success,_ = self.load_ytdl_options()
|
||||||
if not success:
|
if not success:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
def set_runtime_override(self, key, value):
|
||||||
|
self._runtime_overrides[key] = value
|
||||||
|
self.YTDL_OPTIONS[key] = value
|
||||||
|
|
||||||
|
def remove_runtime_override(self, key):
|
||||||
|
self._runtime_overrides.pop(key, None)
|
||||||
|
self.YTDL_OPTIONS.pop(key, None)
|
||||||
|
|
||||||
|
def _apply_runtime_overrides(self):
|
||||||
|
self.YTDL_OPTIONS.update(self._runtime_overrides)
|
||||||
|
|
||||||
def load_ytdl_options(self) -> tuple[bool, str]:
|
def load_ytdl_options(self) -> tuple[bool, str]:
|
||||||
try:
|
try:
|
||||||
self.YTDL_OPTIONS = json.loads(os.environ.get('YTDL_OPTIONS', '{}'))
|
self.YTDL_OPTIONS = json.loads(os.environ.get('YTDL_OPTIONS', '{}'))
|
||||||
@@ -111,6 +124,7 @@ class Config:
|
|||||||
return (False, msg)
|
return (False, msg)
|
||||||
|
|
||||||
if not self.YTDL_OPTIONS_FILE:
|
if not self.YTDL_OPTIONS_FILE:
|
||||||
|
self._apply_runtime_overrides()
|
||||||
return (True, '')
|
return (True, '')
|
||||||
|
|
||||||
log.info(f'Loading yt-dlp custom options from "{self.YTDL_OPTIONS_FILE}"')
|
log.info(f'Loading yt-dlp custom options from "{self.YTDL_OPTIONS_FILE}"')
|
||||||
@@ -128,6 +142,7 @@ class Config:
|
|||||||
return (False, msg)
|
return (False, msg)
|
||||||
|
|
||||||
self.YTDL_OPTIONS.update(opts)
|
self.YTDL_OPTIONS.update(opts)
|
||||||
|
self._apply_runtime_overrides()
|
||||||
return (True, '')
|
return (True, '')
|
||||||
|
|
||||||
config = Config()
|
config = Config()
|
||||||
@@ -347,21 +362,43 @@ async def upload_cookies(request):
|
|||||||
os.remove(COOKIES_PATH)
|
os.remove(COOKIES_PATH)
|
||||||
return web.Response(status=400, text=serializer.encode({'status': 'error', 'msg': 'Cookie file too large (max 1MB)'}))
|
return web.Response(status=400, text=serializer.encode({'status': 'error', 'msg': 'Cookie file too large (max 1MB)'}))
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
config.YTDL_OPTIONS['cookiefile'] = COOKIES_PATH
|
config.set_runtime_override('cookiefile', COOKIES_PATH)
|
||||||
log.info(f'Cookies file uploaded ({size} bytes)')
|
log.info(f'Cookies file uploaded ({size} bytes)')
|
||||||
return web.Response(text=serializer.encode({'status': 'ok', 'msg': f'Cookies uploaded ({size} bytes)'}))
|
return web.Response(text=serializer.encode({'status': 'ok', 'msg': f'Cookies uploaded ({size} bytes)'}))
|
||||||
|
|
||||||
@routes.post(config.URL_PREFIX + 'delete-cookies')
|
@routes.post(config.URL_PREFIX + 'delete-cookies')
|
||||||
async def delete_cookies(request):
|
async def delete_cookies(request):
|
||||||
if os.path.exists(COOKIES_PATH):
|
has_uploaded_cookies = os.path.exists(COOKIES_PATH)
|
||||||
os.remove(COOKIES_PATH)
|
configured_cookiefile = config.YTDL_OPTIONS.get('cookiefile')
|
||||||
config.YTDL_OPTIONS.pop('cookiefile', None)
|
has_manual_cookiefile = isinstance(configured_cookiefile, str) and configured_cookiefile and configured_cookiefile != COOKIES_PATH
|
||||||
|
|
||||||
|
if not has_uploaded_cookies:
|
||||||
|
if has_manual_cookiefile:
|
||||||
|
return web.Response(
|
||||||
|
status=400,
|
||||||
|
text=serializer.encode({
|
||||||
|
'status': 'error',
|
||||||
|
'msg': 'Cookies are configured manually via YTDL_OPTIONS (cookiefile). Remove or change that setting manually; UI delete only removes uploaded cookies.'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return web.Response(status=400, text=serializer.encode({'status': 'error', 'msg': 'No uploaded cookies to delete'}))
|
||||||
|
|
||||||
|
os.remove(COOKIES_PATH)
|
||||||
|
config.remove_runtime_override('cookiefile')
|
||||||
|
success, msg = config.load_ytdl_options()
|
||||||
|
if not success:
|
||||||
|
log.error(f'Cookies file deleted, but failed to reload YTDL_OPTIONS: {msg}')
|
||||||
|
return web.Response(status=500, text=serializer.encode({'status': 'error', 'msg': f'Cookies file deleted, but failed to reload YTDL_OPTIONS: {msg}'}))
|
||||||
|
|
||||||
log.info('Cookies file deleted')
|
log.info('Cookies file deleted')
|
||||||
return web.Response(text=serializer.encode({'status': 'ok'}))
|
return web.Response(text=serializer.encode({'status': 'ok'}))
|
||||||
|
|
||||||
@routes.get(config.URL_PREFIX + 'cookie-status')
|
@routes.get(config.URL_PREFIX + 'cookie-status')
|
||||||
async def cookie_status(request):
|
async def cookie_status(request):
|
||||||
exists = os.path.exists(COOKIES_PATH)
|
configured_cookiefile = config.YTDL_OPTIONS.get('cookiefile')
|
||||||
|
has_configured_cookies = isinstance(configured_cookiefile, str) and os.path.exists(configured_cookiefile)
|
||||||
|
has_uploaded_cookies = os.path.exists(COOKIES_PATH)
|
||||||
|
exists = has_uploaded_cookies or has_configured_cookies
|
||||||
return web.Response(text=serializer.encode({'status': 'ok', 'has_cookies': exists}))
|
return web.Response(text=serializer.encode({'status': 'ok', 'has_cookies': exists}))
|
||||||
|
|
||||||
@routes.get(config.URL_PREFIX + 'history')
|
@routes.get(config.URL_PREFIX + 'history')
|
||||||
@@ -508,7 +545,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
# Auto-detect cookie file on startup
|
# Auto-detect cookie file on startup
|
||||||
if os.path.exists(COOKIES_PATH):
|
if os.path.exists(COOKIES_PATH):
|
||||||
config.YTDL_OPTIONS['cookiefile'] = COOKIES_PATH
|
config.set_runtime_override('cookiefile', COOKIES_PATH)
|
||||||
log.info(f'Cookie file detected at {COOKIES_PATH}')
|
log.info(f'Cookie file detected at {COOKIES_PATH}')
|
||||||
|
|
||||||
if config.HTTPS:
|
if config.HTTPS:
|
||||||
|
|||||||
+63
-40
@@ -211,24 +211,6 @@
|
|||||||
ngbTooltip="Add a prefix to downloaded filenames">
|
ngbTooltip="Add a prefix to downloaded filenames">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="d-flex align-items-center mt-2">
|
|
||||||
<label class="btn btn-sm btn-outline-secondary me-2 mb-0" for="cookie-upload"
|
|
||||||
ngbTooltip="Upload a cookies.txt file for authenticated downloads">
|
|
||||||
<fa-icon [icon]="faUpload" class="me-1" />Cookies
|
|
||||||
</label>
|
|
||||||
<input type="file" id="cookie-upload" class="d-none" accept=".txt"
|
|
||||||
(change)="onCookieFileSelect($event)"
|
|
||||||
[disabled]="cookieUploadInProgress || addInProgress">
|
|
||||||
@if (hasCookies) {
|
|
||||||
<span class="badge bg-success me-2">Active</span>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
|
||||||
(click)="deleteCookies()" ngbTooltip="Remove uploaded cookies">
|
|
||||||
<fa-icon [icon]="faTrashAlt" />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text">Items Limit</span>
|
<span class="input-group-text">Items Limit</span>
|
||||||
@@ -326,30 +308,71 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<hr class="my-3">
|
<hr class="my-3">
|
||||||
<div class="row g-2">
|
<div class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<button type="button"
|
<div class="action-group-label">Cookies</div>
|
||||||
class="btn btn-secondary w-100"
|
<input type="file" id="cookie-upload" class="d-none" accept=".txt"
|
||||||
(click)="openBatchImportModal()">
|
(change)="onCookieFileSelect($event)"
|
||||||
<fa-icon [icon]="faFileImport" class="me-2" />
|
[disabled]="cookieUploadInProgress || addInProgress">
|
||||||
Import URLs
|
<div class="btn-group w-100" role="group">
|
||||||
</button>
|
<label class="btn mb-0"
|
||||||
|
[class]="hasCookies ? 'btn cookie-active-btn mb-0' : 'btn cookie-btn mb-0'"
|
||||||
|
[class.disabled]="cookieUploadInProgress || addInProgress"
|
||||||
|
for="cookie-upload"
|
||||||
|
ngbTooltip="Upload a cookies.txt file for authenticated downloads">
|
||||||
|
@if (cookieUploadInProgress) {
|
||||||
|
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
|
||||||
|
} @else {
|
||||||
|
<fa-icon [icon]="faUpload" class="me-2" />
|
||||||
|
}
|
||||||
|
{{ hasCookies ? 'Replace Cookies' : 'Upload Cookies' }}
|
||||||
|
</label>
|
||||||
|
@if (hasCookies) {
|
||||||
|
<button type="button" class="btn btn-outline-danger"
|
||||||
|
(click)="deleteCookies()"
|
||||||
|
[disabled]="cookieUploadInProgress || addInProgress"
|
||||||
|
ngbTooltip="Remove uploaded cookies">
|
||||||
|
<fa-icon [icon]="faTrashAlt" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="cookie-status" [class.active]="hasCookies">
|
||||||
|
@if (hasCookies) {
|
||||||
|
<fa-icon [icon]="faCheckCircle" class="me-1" />
|
||||||
|
Cookies active
|
||||||
|
} @else {
|
||||||
|
No cookies configured
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-8">
|
||||||
<button type="button"
|
<div class="action-group-label">Bulk Actions</div>
|
||||||
class="btn btn-secondary w-100"
|
<div class="row g-2">
|
||||||
(click)="exportBatchUrls('all')">
|
<div class="col-4">
|
||||||
<fa-icon [icon]="faFileExport" class="me-2" />
|
<button type="button"
|
||||||
Export URLs
|
class="btn btn-secondary w-100"
|
||||||
</button>
|
(click)="openBatchImportModal()">
|
||||||
</div>
|
<fa-icon [icon]="faFileImport" class="me-2" />
|
||||||
<div class="col-md-4">
|
Import URLs
|
||||||
<button type="button"
|
</button>
|
||||||
class="btn btn-secondary w-100"
|
</div>
|
||||||
(click)="copyBatchUrls('all')">
|
<div class="col-4">
|
||||||
<fa-icon [icon]="faCopy" class="me-2" />
|
<button type="button"
|
||||||
Copy URLs
|
class="btn btn-secondary w-100"
|
||||||
</button>
|
(click)="exportBatchUrls('all')">
|
||||||
|
<fa-icon [icon]="faFileExport" class="me-2" />
|
||||||
|
Export URLs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-secondary w-100"
|
||||||
|
(click)="copyBatchUrls('all')">
|
||||||
|
<fa-icon [icon]="faCopy" class="me-2" />
|
||||||
|
Copy URLs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -209,3 +209,48 @@ main
|
|||||||
|
|
||||||
span
|
span
|
||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
|
|
||||||
|
.cookie-btn
|
||||||
|
flex: 1 1 auto
|
||||||
|
background-color: var(--bs-secondary-bg)
|
||||||
|
border-color: var(--bs-border-color)
|
||||||
|
color: var(--bs-emphasis-color)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background-color: var(--bs-tertiary-bg)
|
||||||
|
border-color: var(--bs-secondary)
|
||||||
|
color: var(--bs-emphasis-color)
|
||||||
|
|
||||||
|
&.disabled
|
||||||
|
opacity: 0.65
|
||||||
|
pointer-events: none
|
||||||
|
|
||||||
|
.cookie-active-btn
|
||||||
|
flex: 1 1 auto
|
||||||
|
background-color: var(--bs-success-bg-subtle)
|
||||||
|
border-color: var(--bs-success-border-subtle)
|
||||||
|
color: var(--bs-success-text-emphasis)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background-color: var(--bs-success-bg-subtle)
|
||||||
|
border-color: var(--bs-success)
|
||||||
|
color: var(--bs-success-text-emphasis)
|
||||||
|
|
||||||
|
&.disabled
|
||||||
|
opacity: 0.65
|
||||||
|
pointer-events: none
|
||||||
|
|
||||||
|
.action-group-label
|
||||||
|
font-size: 0.7rem
|
||||||
|
text-transform: uppercase
|
||||||
|
letter-spacing: 0.05em
|
||||||
|
color: var(--bs-secondary-color)
|
||||||
|
margin-bottom: 0.4rem
|
||||||
|
|
||||||
|
.cookie-status
|
||||||
|
font-size: 0.8rem
|
||||||
|
margin-top: 0.35rem
|
||||||
|
color: var(--bs-secondary-color)
|
||||||
|
|
||||||
|
&.active
|
||||||
|
color: var(--bs-success-text-emphasis)
|
||||||
|
|||||||
+48
-4
@@ -793,22 +793,66 @@ export class App implements AfterViewInit, OnInit {
|
|||||||
if (!input.files?.length) return;
|
if (!input.files?.length) return;
|
||||||
this.cookieUploadInProgress = true;
|
this.cookieUploadInProgress = true;
|
||||||
this.downloads.uploadCookies(input.files[0]).subscribe({
|
this.downloads.uploadCookies(input.files[0]).subscribe({
|
||||||
next: () => {
|
next: (response) => {
|
||||||
this.hasCookies = true;
|
if (response?.status === 'ok') {
|
||||||
|
this.hasCookies = true;
|
||||||
|
} else {
|
||||||
|
this.refreshCookieStatus();
|
||||||
|
alert(`Error uploading cookies: ${this.formatErrorMessage(response?.msg)}`);
|
||||||
|
}
|
||||||
this.cookieUploadInProgress = false;
|
this.cookieUploadInProgress = false;
|
||||||
input.value = '';
|
input.value = '';
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
|
this.refreshCookieStatus();
|
||||||
this.cookieUploadInProgress = false;
|
this.cookieUploadInProgress = false;
|
||||||
input.value = '';
|
input.value = '';
|
||||||
|
alert('Error uploading cookies.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatErrorMessage(error: unknown): string {
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
if (error && typeof error === 'object') {
|
||||||
|
const obj = error as Record<string, unknown>;
|
||||||
|
for (const key of ['msg', 'reason', 'error', 'detail']) {
|
||||||
|
const value = obj[key];
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(error);
|
||||||
|
} catch {
|
||||||
|
return 'Unknown error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'Unknown error';
|
||||||
|
}
|
||||||
|
|
||||||
deleteCookies() {
|
deleteCookies() {
|
||||||
this.downloads.deleteCookies().subscribe({
|
this.downloads.deleteCookies().subscribe({
|
||||||
next: () => { this.hasCookies = false; },
|
next: (response) => {
|
||||||
error: () => {}
|
if (response?.status === 'ok') {
|
||||||
|
this.refreshCookieStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.refreshCookieStatus();
|
||||||
|
alert(`Error deleting cookies: ${this.formatErrorMessage(response?.msg)}`);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.refreshCookieStatus();
|
||||||
|
alert('Error deleting cookies.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshCookieStatus() {
|
||||||
|
this.downloads.getCookieStatus().subscribe(data => {
|
||||||
|
this.hasCookies = data?.has_cookies || false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user