forked from DRMTalks/devine
Rework Profile/Authentication System
- Removed `devine auth` command and sub-commands due to lack of support, risk of data, and general quirks of it. - Removed `profiles` config data, you must now specify which profile you wish to use each time with -p/--profile. If you use a specific profile a lot more than others, you should make it the default. See below. - Added a `default` key to each service mapping in `credentials` that will be used if -p/--profile is not specified. - Each service mapping in `credentials` is no longer forced to use profiles. You can now simply specify `Service: username:password` if you only use one credential. - Auth-less Services now simply have to specify no credential and have no cookie file. - There is no longer an error for not having a cookie and/or credential for the chosen profile, as a profile no longer has to be chosen. - Cookies are now checked for in 3 different locations in the following order: 1. `/Cookies/{Service Name}.txt` 2. `/Cookies/Service Name/{profile}.txt` 3. `/Cookies/Service Name/default.txt` This means you now have more options on organization and layout of Cookie files, similarly to the new Credentials config. Note: `/Cookies/Service Name/.txt` also works as an alternative to `default.txt`. The benefit of this is `.txt` will always be at the top of your folder.
This commit is contained in:
parent
1c6e91b6f9
commit
837061cf91
51
CONFIG.md
51
CONFIG.md
|
@ -67,25 +67,30 @@ DSNP:
|
||||||
default: chromecdm_903_l3
|
default: chromecdm_903_l3
|
||||||
```
|
```
|
||||||
|
|
||||||
## credentials (dict)
|
## credentials (dict[str, str|list|dict])
|
||||||
|
|
||||||
Specify login credentials to use for each Service by Profile as Key (case-sensitive).
|
Specify login credentials to use for each Service, and optionally per-profile.
|
||||||
|
|
||||||
The value should be `email:password` or `username:password` (with some exceptions).
|
|
||||||
The first section does not have to be an email or username. It may also be a Phone number.
|
|
||||||
|
|
||||||
For example,
|
For example,
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
AMZN:
|
ALL4: jane@gmail.com:LoremIpsum100 # directly
|
||||||
|
AMZN: # or per-profile, optionally with a default
|
||||||
|
default: jane@example.tld:LoremIpsum99 # <-- used by default if -p/--profile is not used
|
||||||
james: james@gmail.com:TheFriend97
|
james: james@gmail.com:TheFriend97
|
||||||
jane: jane@example.tld:LoremIpsum99
|
|
||||||
john: john@example.tld:LoremIpsum98
|
john: john@example.tld:LoremIpsum98
|
||||||
NF:
|
NF: # the `default` key is not necessary, but no credential will be used by default
|
||||||
john: john@gmail.com:TheGuyWhoPaysForTheNetflix69420
|
john: john@gmail.com:TheGuyWhoPaysForTheNetflix69420
|
||||||
```
|
```
|
||||||
|
|
||||||
Credentials must be specified per-profile. You cannot specify a fallback or default credential.
|
The value should be in string form, i.e. `john@gmail.com:password123` or `john:password123`.
|
||||||
|
Any arbitrary values can be used on the left (username/password/phone) and right (password/secret).
|
||||||
|
You can also specify these in list form, i.e., `["john@gmail.com", ":PasswordWithAColon"]`.
|
||||||
|
|
||||||
|
If you specify multiple credentials with keys like the `AMZN` and `NF` example above, then you should
|
||||||
|
use a `default` key or no credential will be loaded automatically unless you use `-p/--profile`. You
|
||||||
|
do not have to use a `default` key at all.
|
||||||
|
|
||||||
Please be aware that this information is sensitive and to keep it safe. Do not share your config.
|
Please be aware that this information is sensitive and to keep it safe. Do not share your config.
|
||||||
|
|
||||||
## curl_impersonate (dict)
|
## curl_impersonate (dict)
|
||||||
|
@ -260,34 +265,6 @@ together.
|
||||||
- `set_title`
|
- `set_title`
|
||||||
Set the container title to `Show SXXEXX Episode Name` or `Movie (Year)`. Default: `true`
|
Set the container title to `Show SXXEXX Episode Name` or `Movie (Year)`. Default: `true`
|
||||||
|
|
||||||
## profiles (dict)
|
|
||||||
|
|
||||||
Pre-define Profiles to use Per-Service.
|
|
||||||
|
|
||||||
For example,
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
AMZN: jane
|
|
||||||
DSNP: john
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also specify a fallback value to pre-define if a match was not made.
|
|
||||||
This can be done using `default` key. This can help reduce redundancy in your specifications.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
AMZN: jane
|
|
||||||
DSNP: john
|
|
||||||
default: james
|
|
||||||
```
|
|
||||||
|
|
||||||
If a Service doesn't require a profile (as it does not require Credentials or Authorization of any kind), you can
|
|
||||||
disable the profile checks by specifying `false` as the profile for the Service.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
ALL4: false
|
|
||||||
CTV: false
|
|
||||||
```
|
|
||||||
|
|
||||||
## proxy_providers (dict)
|
## proxy_providers (dict)
|
||||||
|
|
||||||
Enable external proxy provider services.
|
Enable external proxy provider services.
|
||||||
|
|
35
README.md
35
README.md
|
@ -252,22 +252,33 @@ sure that the version of devine you have locally is supported by the Service cod
|
||||||
> automatically download. Python importing the files triggers the download to begin. However, it may cause a delay on
|
> automatically download. Python importing the files triggers the download to begin. However, it may cause a delay on
|
||||||
> startup.
|
> startup.
|
||||||
|
|
||||||
## Profiles (Cookies & Credentials)
|
## Cookies & Credentials
|
||||||
|
|
||||||
Just like a streaming service, devine associates both a cookie and/or credential as a Profile. You can associate up to
|
Devine can authenticate with Services using Cookies and/or Credentials. Credentials are stored in the config, and
|
||||||
one cookie and one credential per-profile, depending on which (or both) are needed by the Service. This system allows
|
Cookies are stored in the data directory which can be found by running `devine env info`.
|
||||||
you to configure multiple accounts per-service and choose which to use at any time.
|
|
||||||
|
|
||||||
Credentials are stored in the config, and Cookies are stored in the data directory. You can find the location of these
|
To add a Credential to a Service, take a look at the [Credentials Config](CONFIG.md#credentials-dictstr-strlistdict)
|
||||||
by running `devine env info`. However, you can manage profiles with `devine auth --help`. E.g. to add a new John
|
for information on setting up one or more credentials per-service. You can add one or more Credential per-service and
|
||||||
profile to Netflix with a Cookie and Credential, take a look at the following CLI call,
|
use `-p/--profile` to choose which Credential to use.
|
||||||
`devine auth add John NF --cookie "C:\Users\John\Downloads\netflix.com.txt --credential "john@gmail.com:pass123"`
|
|
||||||
|
|
||||||
You can also delete a credential with `devine auth delete`. E.g., to delete the cookie for John that we just added, run
|
To add a Cookie to a Service, use a Cookie file extension to make a `cookies.txt` file and move it into the Cookies
|
||||||
`devine auth delete John --cookie`. Take a look at `devine auth delete --help` for more information.
|
directory. You must rename the `cookies.txt` file to that of the Service tag (case-sensitive), e.g., `NF.txt`. You can
|
||||||
|
also place it in a Service Cookie folder, e.g., `/Cookies/NF/default.txt` or `/Cookies/NF/.txt`.
|
||||||
|
|
||||||
> __Note__ Profile names are case-sensitive and unique per-service. They also have no arbitrary character or length
|
You can add multiple Cookies to the `/Cookies/NF/` folder with their own unique name and then use `-p/--profile` to
|
||||||
> limit, but for convenience I don't recommend using any special characters as your terminal may get confused.
|
choose which one to use. E.g., `/Cookies/NF/sam.txt` and then use it with `--profile sam`. If you make a Service Cookie
|
||||||
|
folder without a `.txt` or `default.txt`, but with another file, then no Cookies will be loaded unless you use
|
||||||
|
`-p/--profile` like shown. This allows you to opt in to authentication at whim.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> - If your Service does not require Authentication, then do not define any Credential or Cookie for that Service.
|
||||||
|
> - You can use both Cookies and Credentials at the same time, so long as your Service takes and uses both.
|
||||||
|
> - If you are using profiles, then make sure you use the same name on the Credential name and Cookie file name when
|
||||||
|
> using `-p/--profile`.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Profile names are case-sensitive and unique per-service. They have no arbitrary character or length limit, but for
|
||||||
|
> convenience sake I don't recommend using any special characters as your terminal may get confused.
|
||||||
|
|
||||||
### Cookie file format and Extensions
|
### Cookie file format and Extensions
|
||||||
|
|
||||||
|
|
|
@ -1,266 +0,0 @@
|
||||||
import logging
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
import tkinter.filedialog
|
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import click
|
|
||||||
from ruamel.yaml import YAML
|
|
||||||
|
|
||||||
from devine.core.config import Config, config
|
|
||||||
from devine.core.constants import context_settings
|
|
||||||
from devine.core.credential import Credential
|
|
||||||
|
|
||||||
|
|
||||||
@click.group(
|
|
||||||
short_help="Manage cookies and credentials for profiles of services.",
|
|
||||||
context_settings=context_settings)
|
|
||||||
@click.pass_context
|
|
||||||
def auth(ctx: click.Context) -> None:
|
|
||||||
"""Manage cookies and credentials for profiles of services."""
|
|
||||||
ctx.obj = logging.getLogger("auth")
|
|
||||||
|
|
||||||
|
|
||||||
@auth.command(
|
|
||||||
name="list",
|
|
||||||
short_help="List profiles and their state for a service or all services.",
|
|
||||||
context_settings=context_settings)
|
|
||||||
@click.argument("service", type=str, required=False)
|
|
||||||
@click.pass_context
|
|
||||||
def list_(ctx: click.Context, service: Optional[str] = None) -> None:
|
|
||||||
"""
|
|
||||||
List profiles and their state for a service or all services.
|
|
||||||
|
|
||||||
\b
|
|
||||||
Profile and Service names are case-insensitive.
|
|
||||||
"""
|
|
||||||
log = ctx.obj
|
|
||||||
service_f = service
|
|
||||||
|
|
||||||
auth_data: dict[str, dict[str, list]] = defaultdict(lambda: defaultdict(list))
|
|
||||||
|
|
||||||
if config.directories.cookies.exists():
|
|
||||||
for cookie_dir in config.directories.cookies.iterdir():
|
|
||||||
service = cookie_dir.name
|
|
||||||
for cookie in cookie_dir.glob("*.txt"):
|
|
||||||
if cookie.stem not in auth_data[service]:
|
|
||||||
auth_data[service][cookie.stem].append("Cookie")
|
|
||||||
|
|
||||||
for service, credentials in config.credentials.items():
|
|
||||||
for profile in credentials:
|
|
||||||
auth_data[service][profile].append("Credential")
|
|
||||||
|
|
||||||
for service, profiles in dict(sorted(auth_data.items())).items(): # type:ignore
|
|
||||||
if service_f and service != service_f.upper():
|
|
||||||
continue
|
|
||||||
log.info(service)
|
|
||||||
for profile, authorizations in dict(sorted(profiles.items())).items():
|
|
||||||
log.info(f' "{profile}": {", ".join(authorizations)}')
|
|
||||||
|
|
||||||
|
|
||||||
@auth.command(
|
|
||||||
short_help="View profile cookies and credentials for a service.",
|
|
||||||
context_settings=context_settings)
|
|
||||||
@click.argument("profile", type=str)
|
|
||||||
@click.argument("service", type=str)
|
|
||||||
@click.pass_context
|
|
||||||
def view(ctx: click.Context, profile: str, service: str) -> None:
|
|
||||||
"""
|
|
||||||
View profile cookies and credentials for a service.
|
|
||||||
|
|
||||||
\b
|
|
||||||
Profile and Service names are case-sensitive.
|
|
||||||
"""
|
|
||||||
log = ctx.obj
|
|
||||||
service_f = service
|
|
||||||
profile_f = profile
|
|
||||||
found = False
|
|
||||||
|
|
||||||
for cookie_dir in config.directories.cookies.iterdir():
|
|
||||||
if cookie_dir.name == service_f:
|
|
||||||
for cookie in cookie_dir.glob("*.txt"):
|
|
||||||
if cookie.stem == profile_f:
|
|
||||||
log.info(f"Cookie: {cookie}")
|
|
||||||
log.debug(cookie.read_text(encoding="utf8").strip())
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
|
|
||||||
for service, credentials in config.credentials.items():
|
|
||||||
if service == service_f:
|
|
||||||
for profile, credential in credentials.items():
|
|
||||||
if profile == profile_f:
|
|
||||||
log.info(f"Credential: {':'.join(list(credential))}")
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
raise click.ClickException(
|
|
||||||
f"Could not find Profile '{profile_f}' for Service '{service_f}'."
|
|
||||||
f"\nThe profile and service values are case-sensitive."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@auth.command(
|
|
||||||
short_help="Check what profile is used by services.",
|
|
||||||
context_settings=context_settings)
|
|
||||||
@click.argument("service", type=str, required=False)
|
|
||||||
@click.pass_context
|
|
||||||
def status(ctx: click.Context, service: Optional[str] = None) -> None:
|
|
||||||
"""
|
|
||||||
Check what profile is used by services.
|
|
||||||
|
|
||||||
\b
|
|
||||||
Service names are case-sensitive.
|
|
||||||
"""
|
|
||||||
log = ctx.obj
|
|
||||||
found_profile = False
|
|
||||||
for service_, profile in config.profiles.items():
|
|
||||||
if not service or service_.upper() == service.upper():
|
|
||||||
log.info(f"{service_}: {profile or '--'}")
|
|
||||||
found_profile = True
|
|
||||||
|
|
||||||
if not found_profile:
|
|
||||||
log.info(f"No profile has been explicitly set for {service}")
|
|
||||||
|
|
||||||
default = config.profiles.get("default", "not set")
|
|
||||||
log.info(f"The default profile is {default}")
|
|
||||||
|
|
||||||
|
|
||||||
@auth.command(
|
|
||||||
short_help="Delete a profile and all of its authorization from a service.",
|
|
||||||
context_settings=context_settings)
|
|
||||||
@click.argument("profile", type=str)
|
|
||||||
@click.argument("service", type=str)
|
|
||||||
@click.option("--cookie", is_flag=True, default=False, help="Only delete the cookie.")
|
|
||||||
@click.option("--credential", is_flag=True, default=False, help="Only delete the credential.")
|
|
||||||
@click.pass_context
|
|
||||||
def delete(ctx: click.Context, profile: str, service: str, cookie: bool, credential: bool):
|
|
||||||
"""
|
|
||||||
Delete a profile and all of its authorization from a service.
|
|
||||||
|
|
||||||
\b
|
|
||||||
By default this does remove both Cookies and Credentials.
|
|
||||||
You may remove only one of them with --cookie or --credential.
|
|
||||||
|
|
||||||
\b
|
|
||||||
Profile and Service names are case-sensitive.
|
|
||||||
Comments may be removed from config!
|
|
||||||
"""
|
|
||||||
log = ctx.obj
|
|
||||||
service_f = service
|
|
||||||
profile_f = profile
|
|
||||||
found = False
|
|
||||||
|
|
||||||
if not credential:
|
|
||||||
for cookie_dir in config.directories.cookies.iterdir():
|
|
||||||
if cookie_dir.name == service_f:
|
|
||||||
for cookie_ in cookie_dir.glob("*.txt"):
|
|
||||||
if cookie_.stem == profile_f:
|
|
||||||
cookie_.unlink()
|
|
||||||
log.info(f"Deleted Cookie: {cookie_}")
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not cookie:
|
|
||||||
for key, credentials in config.credentials.items():
|
|
||||||
if key == service_f:
|
|
||||||
for profile, credential_ in credentials.items():
|
|
||||||
if profile == profile_f:
|
|
||||||
config_path = Config._Directories.user_configs / Config._Filenames.root_config
|
|
||||||
yaml, data = YAML(), None
|
|
||||||
yaml.default_flow_style = False
|
|
||||||
data = yaml.load(config_path)
|
|
||||||
del data["credentials"][key][profile_f]
|
|
||||||
yaml.dump(data, config_path)
|
|
||||||
log.info(f"Deleted Credential: {credential_}")
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
raise click.ClickException(
|
|
||||||
f"Could not find Profile '{profile_f}' for Service '{service_f}'."
|
|
||||||
f"\nThe profile and service values are case-sensitive."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@auth.command(
|
|
||||||
short_help="Add a Credential and/or Cookies to an existing or new profile for a service.",
|
|
||||||
context_settings=context_settings)
|
|
||||||
@click.argument("profile", type=str)
|
|
||||||
@click.argument("service", type=str)
|
|
||||||
@click.option("--cookie", type=str, default=None, help="Direct path to Cookies to add.")
|
|
||||||
@click.option("--credential", type=str, default=None, help="Direct Credential string to add.")
|
|
||||||
@click.pass_context
|
|
||||||
def add(ctx: click.Context, profile: str, service: str, cookie: Optional[str] = None, credential: Optional[str] = None):
|
|
||||||
"""
|
|
||||||
Add a Credential and/or Cookies to an existing or new profile for a service.
|
|
||||||
|
|
||||||
\b
|
|
||||||
Cancel the Open File dialogue when presented if you do not wish to provide
|
|
||||||
cookies. The Credential should be in `Username:Password` form. The username
|
|
||||||
may be an email. If you do not wish to add a Credential, just hit enter.
|
|
||||||
|
|
||||||
\b
|
|
||||||
Profile and Service names are case-sensitive!
|
|
||||||
Comments may be removed from config!
|
|
||||||
"""
|
|
||||||
log = ctx.obj
|
|
||||||
service = service.upper()
|
|
||||||
profile = profile.lower()
|
|
||||||
|
|
||||||
if cookie:
|
|
||||||
cookie = Path(cookie)
|
|
||||||
if not cookie.is_file():
|
|
||||||
log.error(f"No such file or directory: {cookie}.")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
print("Opening File Dialogue, select a Cookie file to import.")
|
|
||||||
cookie = tkinter.filedialog.askopenfilename(
|
|
||||||
title="Select a Cookie file (Cancel to skip)",
|
|
||||||
filetypes=[("Cookies", "*.txt"), ("All files", "*.*")]
|
|
||||||
)
|
|
||||||
if cookie:
|
|
||||||
cookie = Path(cookie)
|
|
||||||
else:
|
|
||||||
log.info("Skipped adding a Cookie...")
|
|
||||||
|
|
||||||
if credential:
|
|
||||||
try:
|
|
||||||
credential = Credential.loads(credential)
|
|
||||||
except ValueError as e:
|
|
||||||
raise click.ClickException(str(e))
|
|
||||||
else:
|
|
||||||
credential = input("Credential: ")
|
|
||||||
if credential:
|
|
||||||
try:
|
|
||||||
credential = Credential.loads(credential)
|
|
||||||
except ValueError as e:
|
|
||||||
raise click.ClickException(str(e))
|
|
||||||
else:
|
|
||||||
log.info("Skipped adding a Credential...")
|
|
||||||
|
|
||||||
if cookie:
|
|
||||||
final_path = (config.directories.cookies / service / profile).with_suffix(".txt")
|
|
||||||
final_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
if final_path.exists():
|
|
||||||
log.error(f"A Cookie file for the Profile {profile} on {service} already exists.")
|
|
||||||
sys.exit(1)
|
|
||||||
shutil.move(cookie, final_path)
|
|
||||||
log.info(f"Moved Cookie file to: {final_path}")
|
|
||||||
|
|
||||||
if credential:
|
|
||||||
config_path = Config._Directories.user_configs / Config._Filenames.root_config
|
|
||||||
yaml, data = YAML(), None
|
|
||||||
yaml.default_flow_style = False
|
|
||||||
data = yaml.load(config_path)
|
|
||||||
if not data:
|
|
||||||
data = {}
|
|
||||||
if "credentials" not in data:
|
|
||||||
data["credentials"] = {}
|
|
||||||
if service not in data["credentials"]:
|
|
||||||
data["credentials"][service] = {}
|
|
||||||
data["credentials"][service][profile] = credential.dumps()
|
|
||||||
yaml.dump(data, config_path)
|
|
||||||
log.info(f"Added Credential: {credential}")
|
|
|
@ -28,6 +28,7 @@ from pymediainfo import MediaInfo
|
||||||
from pywidevine.cdm import Cdm as WidevineCdm
|
from pywidevine.cdm import Cdm as WidevineCdm
|
||||||
from pywidevine.device import Device
|
from pywidevine.device import Device
|
||||||
from pywidevine.remotecdm import RemoteCdm
|
from pywidevine.remotecdm import RemoteCdm
|
||||||
|
from requests.cookies import RequestsCookieJar
|
||||||
from rich.console import Group
|
from rich.console import Group
|
||||||
from rich.live import Live
|
from rich.live import Live
|
||||||
from rich.padding import Padding
|
from rich.padding import Padding
|
||||||
|
@ -68,7 +69,7 @@ class dl:
|
||||||
token_normalize_func=Services.get_tag
|
token_normalize_func=Services.get_tag
|
||||||
))
|
))
|
||||||
@click.option("-p", "--profile", type=str, default=None,
|
@click.option("-p", "--profile", type=str, default=None,
|
||||||
help="Profile to use for Credentials and Cookies (if available). Overrides profile set by config.")
|
help="Profile to use for Credentials and Cookies (if available).")
|
||||||
@click.option("-q", "--quality", type=QUALITY_LIST, default=[],
|
@click.option("-q", "--quality", type=QUALITY_LIST, default=[],
|
||||||
help="Download Resolution(s), defaults to the best available resolution.")
|
help="Download Resolution(s), defaults to the best available resolution.")
|
||||||
@click.option("-v", "--vcodec", type=click.Choice(Video.Codec, case_sensitive=False),
|
@click.option("-v", "--vcodec", type=click.Choice(Video.Codec, case_sensitive=False),
|
||||||
|
@ -155,17 +156,14 @@ class dl:
|
||||||
self.log = logging.getLogger("download")
|
self.log = logging.getLogger("download")
|
||||||
|
|
||||||
self.service = Services.get_tag(ctx.invoked_subcommand)
|
self.service = Services.get_tag(ctx.invoked_subcommand)
|
||||||
|
self.profile = profile
|
||||||
|
|
||||||
with console.status("Preparing Service and Profile Authentication...", spinner="dots"):
|
if self.profile:
|
||||||
if profile:
|
self.log.info(f"Using profile: '{self.profile}'")
|
||||||
self.profile = profile
|
|
||||||
self.log.info(f"Profile: '{self.profile}' from the --profile argument")
|
|
||||||
else:
|
|
||||||
self.profile = self.get_profile(self.service)
|
|
||||||
self.log.info(f"Profile: '{self.profile}' from the config")
|
|
||||||
|
|
||||||
|
with console.status("Loading Service Config...", spinner="dots"):
|
||||||
service_config_path = Services.get_path(self.service) / config.filenames.config
|
service_config_path = Services.get_path(self.service) / config.filenames.config
|
||||||
if service_config_path.is_file():
|
if service_config_path.exists():
|
||||||
self.service_config = yaml.safe_load(service_config_path.read_text(encoding="utf8"))
|
self.service_config = yaml.safe_load(service_config_path.read_text(encoding="utf8"))
|
||||||
self.log.info("Service Config loaded")
|
self.log.info("Service Config loaded")
|
||||||
else:
|
else:
|
||||||
|
@ -289,14 +287,11 @@ class dl:
|
||||||
else:
|
else:
|
||||||
vaults_only = not cdm_only
|
vaults_only = not cdm_only
|
||||||
|
|
||||||
if self.profile:
|
with console.status("Authenticating with Service...", spinner="dots"):
|
||||||
with console.status("Authenticating with Service...", spinner="dots"):
|
cookies = self.get_cookie_jar(self.service, self.profile)
|
||||||
cookies = self.get_cookie_jar(self.service, self.profile)
|
credential = self.get_credentials(self.service, self.profile)
|
||||||
credential = self.get_credentials(self.service, self.profile)
|
service.authenticate(cookies, credential)
|
||||||
if not cookies and not credential:
|
if cookies or credential:
|
||||||
self.log.error(f"The Profile '{self.profile}' has no Cookies or Credentials, Check for typos")
|
|
||||||
sys.exit(1)
|
|
||||||
service.authenticate(cookies, credential)
|
|
||||||
self.log.info("Authenticated with Service")
|
self.log.info("Authenticated with Service")
|
||||||
|
|
||||||
with console.status("Fetching Title Metadata...", spinner="dots"):
|
with console.status("Fetching Title Metadata...", spinner="dots"):
|
||||||
|
@ -663,13 +658,9 @@ class dl:
|
||||||
))
|
))
|
||||||
|
|
||||||
# update cookies
|
# update cookies
|
||||||
cookie_file = config.directories.cookies / service.__class__.__name__ / f"{self.profile}.txt"
|
cookie_file = self.get_cookie_path(self.service, self.profile)
|
||||||
if cookie_file.exists():
|
if cookie_file.exists():
|
||||||
cookie_jar = MozillaCookieJar(cookie_file)
|
self.save_cookies(cookie_file, service.session.cookies)
|
||||||
cookie_jar.load()
|
|
||||||
for cookie in service.session.cookies:
|
|
||||||
cookie_jar.set_cookie(cookie)
|
|
||||||
cookie_jar.save(ignore_discard=True)
|
|
||||||
|
|
||||||
dl_time = time_elapsed_since(start_time)
|
dl_time = time_elapsed_since(start_time)
|
||||||
|
|
||||||
|
@ -954,10 +945,24 @@ class dl:
|
||||||
return profile
|
return profile
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cookie_jar(service: str, profile: str) -> Optional[MozillaCookieJar]:
|
def get_cookie_path(service: str, profile: Optional[str]) -> Optional[Path]:
|
||||||
"""Get Profile's Cookies as Mozilla Cookie Jar if available."""
|
"""Get Service Cookie File Path for Profile."""
|
||||||
cookie_file = config.directories.cookies / service / f"{profile}.txt"
|
direct_cookie_file = config.directories.cookies / f"{service}.txt"
|
||||||
if cookie_file.is_file():
|
profile_cookie_file = config.directories.cookies / service / f"{profile}.txt"
|
||||||
|
default_cookie_file = config.directories.cookies / service / "default.txt"
|
||||||
|
|
||||||
|
if direct_cookie_file.exists():
|
||||||
|
return direct_cookie_file
|
||||||
|
elif profile_cookie_file.exists():
|
||||||
|
return profile_cookie_file
|
||||||
|
elif default_cookie_file.exists():
|
||||||
|
return default_cookie_file
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_cookie_jar(service: str, profile: Optional[str]) -> Optional[MozillaCookieJar]:
|
||||||
|
"""Get Service Cookies for Profile."""
|
||||||
|
cookie_file = dl.get_cookie_path(service, profile)
|
||||||
|
if cookie_file:
|
||||||
cookie_jar = MozillaCookieJar(cookie_file)
|
cookie_jar = MozillaCookieJar(cookie_file)
|
||||||
cookie_data = html.unescape(cookie_file.read_text("utf8")).splitlines(keepends=False)
|
cookie_data = html.unescape(cookie_file.read_text("utf8")).splitlines(keepends=False)
|
||||||
for i, line in enumerate(cookie_data):
|
for i, line in enumerate(cookie_data):
|
||||||
|
@ -972,17 +977,29 @@ class dl:
|
||||||
cookie_file.write_text(cookie_data, "utf8")
|
cookie_file.write_text(cookie_data, "utf8")
|
||||||
cookie_jar.load(ignore_discard=True, ignore_expires=True)
|
cookie_jar.load(ignore_discard=True, ignore_expires=True)
|
||||||
return cookie_jar
|
return cookie_jar
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_credentials(service: str, profile: str) -> Optional[Credential]:
|
def save_cookies(path: Path, cookies: RequestsCookieJar):
|
||||||
"""Get Profile's Credential if available."""
|
cookie_jar = MozillaCookieJar(path)
|
||||||
cred = config.credentials.get(service, {}).get(profile)
|
cookie_jar.load()
|
||||||
if cred:
|
for cookie in cookies:
|
||||||
if isinstance(cred, list):
|
cookie_jar.set_cookie(cookie)
|
||||||
return Credential(*cred)
|
cookie_jar.save(ignore_discard=True)
|
||||||
return Credential.loads(cred)
|
|
||||||
return None
|
@staticmethod
|
||||||
|
def get_credentials(service: str, profile: Optional[str]) -> Optional[Credential]:
|
||||||
|
"""Get Service Credentials for Profile."""
|
||||||
|
credentials = config.credentials.get(service)
|
||||||
|
if credentials:
|
||||||
|
if isinstance(credentials, dict):
|
||||||
|
if profile:
|
||||||
|
credentials = credentials.get(profile) or credentials.get("default")
|
||||||
|
else:
|
||||||
|
credentials = credentials.get("default")
|
||||||
|
if credentials:
|
||||||
|
if isinstance(credentials, list):
|
||||||
|
return Credential(*credentials)
|
||||||
|
return Credential.loads(credentials) # type: ignore
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cdm(service: str, profile: Optional[str] = None) -> WidevineCdm:
|
def get_cdm(service: str, profile: Optional[str] = None) -> WidevineCdm:
|
||||||
|
|
|
@ -60,7 +60,6 @@ class Config:
|
||||||
self.key_vaults: list[dict[str, Any]] = kwargs.get("key_vaults", [])
|
self.key_vaults: list[dict[str, Any]] = kwargs.get("key_vaults", [])
|
||||||
self.muxing: dict = kwargs.get("muxing") or {}
|
self.muxing: dict = kwargs.get("muxing") or {}
|
||||||
self.nordvpn: dict = kwargs.get("nordvpn") or {}
|
self.nordvpn: dict = kwargs.get("nordvpn") or {}
|
||||||
self.profiles: dict = kwargs.get("profiles") or {}
|
|
||||||
self.proxy_providers: dict = kwargs.get("proxy_providers") or {}
|
self.proxy_providers: dict = kwargs.get("proxy_providers") or {}
|
||||||
self.serve: dict = kwargs.get("serve") or {}
|
self.serve: dict = kwargs.get("serve") or {}
|
||||||
self.services: dict = kwargs.get("services") or {}
|
self.services: dict = kwargs.get("services") or {}
|
||||||
|
|
Loading…
Reference in New Issue