mirror of
				https://github.com/devine-dl/devine.git
				synced 2025-11-04 03:44:49 +00:00 
			
		
		
		
	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
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## credentials (dict)
 | 
			
		||||
## credentials (dict[str, str|list|dict])
 | 
			
		||||
 | 
			
		||||
Specify login credentials to use for each Service by Profile as Key (case-sensitive).
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
Specify login credentials to use for each Service, and optionally per-profile.
 | 
			
		||||
 | 
			
		||||
For example,
 | 
			
		||||
 | 
			
		||||
```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
 | 
			
		||||
  jane: jane@example.tld:LoremIpsum99
 | 
			
		||||
  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
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
## curl_impersonate (dict)
 | 
			
		||||
@ -260,34 +265,6 @@ together.
 | 
			
		||||
- `set_title`
 | 
			
		||||
  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)
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
> 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
 | 
			
		||||
one cookie and one credential per-profile, depending on which (or both) are needed by the Service. This system allows
 | 
			
		||||
you to configure multiple accounts per-service and choose which to use at any time.
 | 
			
		||||
Devine can authenticate with Services using Cookies and/or Credentials. Credentials are stored in the config, and
 | 
			
		||||
Cookies are stored in the data directory which can be found by running `devine env info`.
 | 
			
		||||
 | 
			
		||||
Credentials are stored in the config, and Cookies are stored in the data directory. You can find the location of these
 | 
			
		||||
by running `devine env info`. However, you can manage profiles with `devine auth --help`. E.g. to add a new John
 | 
			
		||||
profile to Netflix with a Cookie and Credential, take a look at the following CLI call,
 | 
			
		||||
`devine auth add John NF --cookie "C:\Users\John\Downloads\netflix.com.txt --credential "john@gmail.com:pass123"`
 | 
			
		||||
To add a Credential to a Service, take a look at the [Credentials Config](CONFIG.md#credentials-dictstr-strlistdict)
 | 
			
		||||
for information on setting up one or more credentials per-service. You can add one or more Credential per-service and
 | 
			
		||||
use `-p/--profile` to choose which Credential to use.
 | 
			
		||||
 | 
			
		||||
You can also delete a credential with `devine auth delete`. E.g., to delete the cookie for John that we just added, run
 | 
			
		||||
`devine auth delete John --cookie`. Take a look at `devine auth delete --help` for more information.
 | 
			
		||||
To add a Cookie to a Service, use a Cookie file extension to make a `cookies.txt` file and move it into the Cookies
 | 
			
		||||
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
 | 
			
		||||
> limit, but for convenience I don't recommend using any special characters as your terminal may get confused.
 | 
			
		||||
You can add multiple Cookies to the `/Cookies/NF/` folder with their own unique name and then use `-p/--profile` to
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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.device import Device
 | 
			
		||||
from pywidevine.remotecdm import RemoteCdm
 | 
			
		||||
from requests.cookies import RequestsCookieJar
 | 
			
		||||
from rich.console import Group
 | 
			
		||||
from rich.live import Live
 | 
			
		||||
from rich.padding import Padding
 | 
			
		||||
@ -68,7 +69,7 @@ class dl:
 | 
			
		||||
            token_normalize_func=Services.get_tag
 | 
			
		||||
        ))
 | 
			
		||||
    @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=[],
 | 
			
		||||
                  help="Download Resolution(s), defaults to the best available resolution.")
 | 
			
		||||
    @click.option("-v", "--vcodec", type=click.Choice(Video.Codec, case_sensitive=False),
 | 
			
		||||
@ -155,17 +156,14 @@ class dl:
 | 
			
		||||
        self.log = logging.getLogger("download")
 | 
			
		||||
 | 
			
		||||
        self.service = Services.get_tag(ctx.invoked_subcommand)
 | 
			
		||||
 | 
			
		||||
        with console.status("Preparing Service and Profile Authentication...", spinner="dots"):
 | 
			
		||||
            if 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")
 | 
			
		||||
 | 
			
		||||
        if self.profile:
 | 
			
		||||
            self.log.info(f"Using profile: '{self.profile}'")
 | 
			
		||||
 | 
			
		||||
        with console.status("Loading Service Config...", spinner="dots"):
 | 
			
		||||
            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.log.info("Service Config loaded")
 | 
			
		||||
            else:
 | 
			
		||||
@ -289,14 +287,11 @@ class dl:
 | 
			
		||||
        else:
 | 
			
		||||
            vaults_only = not cdm_only
 | 
			
		||||
 | 
			
		||||
        if self.profile:
 | 
			
		||||
        with console.status("Authenticating with Service...", spinner="dots"):
 | 
			
		||||
            cookies = self.get_cookie_jar(self.service, self.profile)
 | 
			
		||||
            credential = self.get_credentials(self.service, self.profile)
 | 
			
		||||
                if not cookies and not credential:
 | 
			
		||||
                    self.log.error(f"The Profile '{self.profile}' has no Cookies or Credentials, Check for typos")
 | 
			
		||||
                    sys.exit(1)
 | 
			
		||||
            service.authenticate(cookies, credential)
 | 
			
		||||
            if cookies or credential:
 | 
			
		||||
                self.log.info("Authenticated with Service")
 | 
			
		||||
 | 
			
		||||
        with console.status("Fetching Title Metadata...", spinner="dots"):
 | 
			
		||||
@ -663,13 +658,9 @@ class dl:
 | 
			
		||||
                ))
 | 
			
		||||
 | 
			
		||||
            # 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():
 | 
			
		||||
                cookie_jar = MozillaCookieJar(cookie_file)
 | 
			
		||||
                cookie_jar.load()
 | 
			
		||||
                for cookie in service.session.cookies:
 | 
			
		||||
                    cookie_jar.set_cookie(cookie)
 | 
			
		||||
                cookie_jar.save(ignore_discard=True)
 | 
			
		||||
                self.save_cookies(cookie_file, service.session.cookies)
 | 
			
		||||
 | 
			
		||||
        dl_time = time_elapsed_since(start_time)
 | 
			
		||||
 | 
			
		||||
@ -954,10 +945,24 @@ class dl:
 | 
			
		||||
        return profile
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_cookie_jar(service: str, profile: str) -> Optional[MozillaCookieJar]:
 | 
			
		||||
        """Get Profile's Cookies as Mozilla Cookie Jar if available."""
 | 
			
		||||
        cookie_file = config.directories.cookies / service / f"{profile}.txt"
 | 
			
		||||
        if cookie_file.is_file():
 | 
			
		||||
    def get_cookie_path(service: str, profile: Optional[str]) -> Optional[Path]:
 | 
			
		||||
        """Get Service Cookie File Path for Profile."""
 | 
			
		||||
        direct_cookie_file = config.directories.cookies / f"{service}.txt"
 | 
			
		||||
        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_data = html.unescape(cookie_file.read_text("utf8")).splitlines(keepends=False)
 | 
			
		||||
            for i, line in enumerate(cookie_data):
 | 
			
		||||
@ -972,17 +977,29 @@ class dl:
 | 
			
		||||
            cookie_file.write_text(cookie_data, "utf8")
 | 
			
		||||
            cookie_jar.load(ignore_discard=True, ignore_expires=True)
 | 
			
		||||
            return cookie_jar
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_credentials(service: str, profile: str) -> Optional[Credential]:
 | 
			
		||||
        """Get Profile's Credential if available."""
 | 
			
		||||
        cred = config.credentials.get(service, {}).get(profile)
 | 
			
		||||
        if cred:
 | 
			
		||||
            if isinstance(cred, list):
 | 
			
		||||
                return Credential(*cred)
 | 
			
		||||
            return Credential.loads(cred)
 | 
			
		||||
        return None
 | 
			
		||||
    def save_cookies(path: Path, cookies: RequestsCookieJar):
 | 
			
		||||
        cookie_jar = MozillaCookieJar(path)
 | 
			
		||||
        cookie_jar.load()
 | 
			
		||||
        for cookie in cookies:
 | 
			
		||||
            cookie_jar.set_cookie(cookie)
 | 
			
		||||
        cookie_jar.save(ignore_discard=True)
 | 
			
		||||
 | 
			
		||||
    @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
 | 
			
		||||
    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.muxing: dict = kwargs.get("muxing") 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.serve: dict = kwargs.get("serve") or {}
 | 
			
		||||
        self.services: dict = kwargs.get("services") or {}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user