mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-03-17 08:37:31 +00:00
Compare commits
3 Commits
6137146705
...
a82828768d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a82828768d | ||
|
|
d18a5de0d0 | ||
|
|
04b540b363 |
@@ -1147,8 +1147,9 @@ class dl:
|
|||||||
with Live(Padding(progress, (0, 5, 1, 5)), console=console):
|
with Live(Padding(progress, (0, 5, 1, 5)), console=console):
|
||||||
for task_id, task_tracks in multiplex_tasks:
|
for task_id, task_tracks in multiplex_tasks:
|
||||||
progress.start_task(task_id) # TODO: Needed?
|
progress.start_task(task_id) # TODO: Needed?
|
||||||
|
audio_expected = not video_only and not no_audio
|
||||||
muxed_path, return_code, errors = task_tracks.mux(
|
muxed_path, return_code, errors = task_tracks.mux(
|
||||||
str(title), progress=partial(progress.update, task_id=task_id), delete=False
|
str(title), progress=partial(progress.update, task_id=task_id), delete=False, audio_expected=audio_expected, title_language=title.language
|
||||||
)
|
)
|
||||||
muxed_paths.append(muxed_path)
|
muxed_paths.append(muxed_path)
|
||||||
if return_code >= 2:
|
if return_code >= 2:
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ from pywidevine.cdm import Cdm as WidevineCdm
|
|||||||
from pywidevine.device import DeviceTypes
|
from pywidevine.device import DeviceTypes
|
||||||
from requests import Session
|
from requests import Session
|
||||||
|
|
||||||
from unshackle.core.vaults import Vaults
|
|
||||||
from unshackle.core import __version__
|
from unshackle.core import __version__
|
||||||
|
from unshackle.core.vaults import Vaults
|
||||||
|
|
||||||
|
|
||||||
class MockCertificateChain:
|
class MockCertificateChain:
|
||||||
@@ -353,17 +353,19 @@ class DecryptLabsRemoteCDM:
|
|||||||
Generate a license challenge using Decrypt Labs API with intelligent caching.
|
Generate a license challenge using Decrypt Labs API with intelligent caching.
|
||||||
|
|
||||||
This method implements smart caching logic that:
|
This method implements smart caching logic that:
|
||||||
1. First attempts to retrieve cached keys from the API
|
1. First checks local vaults for required keys
|
||||||
2. If required KIDs are set, compares cached keys against requirements
|
2. Attempts to retrieve cached keys from the API
|
||||||
3. Only makes a license request if keys are missing
|
3. If required KIDs are set, compares available keys (vault + cached) against requirements
|
||||||
4. Returns empty challenge if all required keys are cached
|
4. Only makes a license request if keys are missing
|
||||||
|
5. Returns empty challenge if all required keys are available
|
||||||
|
|
||||||
The intelligent caching works as follows:
|
The intelligent caching works as follows:
|
||||||
|
- Local vaults: Always checked first if available
|
||||||
- For L1/L2 devices: Always prioritizes cached keys (API automatically optimizes)
|
- For L1/L2 devices: Always prioritizes cached keys (API automatically optimizes)
|
||||||
- For other devices: Uses cache retry logic based on session state
|
- For other devices: Uses cache retry logic based on session state
|
||||||
- With required KIDs set: Only requests license for missing keys
|
- With required KIDs set: Only requests license for missing keys
|
||||||
- Without required KIDs: Returns any available cached keys
|
- Without required KIDs: Returns any available cached keys
|
||||||
- For PlayReady: Combines cached keys with license keys seamlessly
|
- For PlayReady: Combines vault, cached, and license keys seamlessly
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id: Session identifier
|
session_id: Session identifier
|
||||||
@@ -372,7 +374,7 @@ class DecryptLabsRemoteCDM:
|
|||||||
privacy_mode: Whether to use privacy mode - for compatibility only
|
privacy_mode: Whether to use privacy mode - for compatibility only
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
License challenge as bytes, or empty bytes if cached keys satisfy requirements
|
License challenge as bytes, or empty bytes if available keys satisfy requirements
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
InvalidSession: If session ID is invalid
|
InvalidSession: If session ID is invalid
|
||||||
@@ -381,6 +383,7 @@ class DecryptLabsRemoteCDM:
|
|||||||
Note:
|
Note:
|
||||||
Call set_required_kids() before this method for optimal caching behavior.
|
Call set_required_kids() before this method for optimal caching behavior.
|
||||||
L1/L2 devices automatically use cached keys when available per API design.
|
L1/L2 devices automatically use cached keys when available per API design.
|
||||||
|
Local vault keys are always checked first when vaults are available.
|
||||||
"""
|
"""
|
||||||
_ = license_type, privacy_mode
|
_ = license_type, privacy_mode
|
||||||
|
|
||||||
@@ -393,6 +396,31 @@ class DecryptLabsRemoteCDM:
|
|||||||
init_data = self._get_init_data_from_pssh(pssh_or_wrm)
|
init_data = self._get_init_data_from_pssh(pssh_or_wrm)
|
||||||
already_tried_cache = session.get("tried_cache", False)
|
already_tried_cache = session.get("tried_cache", False)
|
||||||
|
|
||||||
|
if self.vaults and self._required_kids:
|
||||||
|
vault_keys = []
|
||||||
|
for kid_str in self._required_kids:
|
||||||
|
try:
|
||||||
|
clean_kid = kid_str.replace("-", "")
|
||||||
|
if len(clean_kid) == 32:
|
||||||
|
kid_uuid = UUID(hex=clean_kid)
|
||||||
|
else:
|
||||||
|
kid_uuid = UUID(hex=clean_kid.ljust(32, "0"))
|
||||||
|
key, _ = self.vaults.get_key(kid_uuid)
|
||||||
|
if key and key.count("0") != len(key):
|
||||||
|
vault_keys.append({"kid": kid_str, "key": key, "type": "CONTENT"})
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if vault_keys:
|
||||||
|
vault_kids = set(k["kid"] for k in vault_keys)
|
||||||
|
required_kids = set(self._required_kids)
|
||||||
|
|
||||||
|
if required_kids.issubset(vault_kids):
|
||||||
|
session["keys"] = vault_keys
|
||||||
|
return b""
|
||||||
|
else:
|
||||||
|
session["vault_keys"] = vault_keys
|
||||||
|
|
||||||
if self.device_name in ["L1", "L2"]:
|
if self.device_name in ["L1", "L2"]:
|
||||||
get_cached_keys = True
|
get_cached_keys = True
|
||||||
else:
|
else:
|
||||||
@@ -404,7 +432,7 @@ class DecryptLabsRemoteCDM:
|
|||||||
"get_cached_keys_if_exists": get_cached_keys,
|
"get_cached_keys_if_exists": get_cached_keys,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.device_name in ["L1", "L2", "SL2", "SL3"] and self.service_name:
|
if self.service_name:
|
||||||
request_data["service"] = self.service_name
|
request_data["service"] = self.service_name
|
||||||
|
|
||||||
if session["service_certificate"]:
|
if session["service_certificate"]:
|
||||||
@@ -441,17 +469,22 @@ class DecryptLabsRemoteCDM:
|
|||||||
"""
|
"""
|
||||||
cached_keys = data.get("cached_keys", [])
|
cached_keys = data.get("cached_keys", [])
|
||||||
parsed_keys = self._parse_cached_keys(cached_keys)
|
parsed_keys = self._parse_cached_keys(cached_keys)
|
||||||
session["keys"] = parsed_keys
|
|
||||||
|
all_available_keys = list(parsed_keys)
|
||||||
|
if "vault_keys" in session:
|
||||||
|
all_available_keys.extend(session["vault_keys"])
|
||||||
|
|
||||||
|
session["keys"] = all_available_keys
|
||||||
session["tried_cache"] = True
|
session["tried_cache"] = True
|
||||||
|
|
||||||
if self._required_kids:
|
if self._required_kids:
|
||||||
cached_kids = set()
|
available_kids = set()
|
||||||
for key in parsed_keys:
|
for key in all_available_keys:
|
||||||
if isinstance(key, dict) and "kid" in key:
|
if isinstance(key, dict) and "kid" in key:
|
||||||
cached_kids.add(key["kid"].replace("-", "").lower())
|
available_kids.add(key["kid"].replace("-", "").lower())
|
||||||
|
|
||||||
required_kids = set(self._required_kids)
|
required_kids = set(self._required_kids)
|
||||||
missing_kids = required_kids - cached_kids
|
missing_kids = required_kids - available_kids
|
||||||
|
|
||||||
if missing_kids:
|
if missing_kids:
|
||||||
session["cached_keys"] = parsed_keys
|
session["cached_keys"] = parsed_keys
|
||||||
@@ -585,15 +618,15 @@ class DecryptLabsRemoteCDM:
|
|||||||
|
|
||||||
license_keys = self._parse_keys_response(data)
|
license_keys = self._parse_keys_response(data)
|
||||||
|
|
||||||
if self.is_playready and "cached_keys" in session:
|
all_keys = []
|
||||||
"""
|
|
||||||
Combine cached keys with license keys for PlayReady content.
|
|
||||||
|
|
||||||
This ensures we have both the cached keys (obtained earlier) and
|
if "vault_keys" in session:
|
||||||
any additional keys from the license response, without duplicates.
|
all_keys.extend(session["vault_keys"])
|
||||||
"""
|
|
||||||
|
if "cached_keys" in session:
|
||||||
cached_keys = session.get("cached_keys", [])
|
cached_keys = session.get("cached_keys", [])
|
||||||
all_keys = list(cached_keys)
|
for cached_key in cached_keys:
|
||||||
|
all_keys.append(cached_key)
|
||||||
|
|
||||||
for license_key in license_keys:
|
for license_key in license_keys:
|
||||||
already_exists = False
|
already_exists = False
|
||||||
@@ -606,16 +639,16 @@ class DecryptLabsRemoteCDM:
|
|||||||
license_kid = str(license_key.key_id).replace("-", "").lower()
|
license_kid = str(license_key.key_id).replace("-", "").lower()
|
||||||
|
|
||||||
if license_kid:
|
if license_kid:
|
||||||
for cached_key in cached_keys:
|
for existing_key in all_keys:
|
||||||
cached_kid = None
|
existing_kid = None
|
||||||
if isinstance(cached_key, dict) and "kid" in cached_key:
|
if isinstance(existing_key, dict) and "kid" in existing_key:
|
||||||
cached_kid = cached_key["kid"].replace("-", "").lower()
|
existing_kid = existing_key["kid"].replace("-", "").lower()
|
||||||
elif hasattr(cached_key, "kid"):
|
elif hasattr(existing_key, "kid"):
|
||||||
cached_kid = str(cached_key.kid).replace("-", "").lower()
|
existing_kid = str(existing_key.kid).replace("-", "").lower()
|
||||||
elif hasattr(cached_key, "key_id"):
|
elif hasattr(existing_key, "key_id"):
|
||||||
cached_kid = str(cached_key.key_id).replace("-", "").lower()
|
existing_kid = str(existing_key.key_id).replace("-", "").lower()
|
||||||
|
|
||||||
if cached_kid == license_kid:
|
if existing_kid == license_kid:
|
||||||
already_exists = True
|
already_exists = True
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -623,12 +656,23 @@ class DecryptLabsRemoteCDM:
|
|||||||
all_keys.append(license_key)
|
all_keys.append(license_key)
|
||||||
|
|
||||||
session["keys"] = all_keys
|
session["keys"] = all_keys
|
||||||
session["cached_keys"] = None
|
session.pop("cached_keys", None)
|
||||||
else:
|
session.pop("vault_keys", None)
|
||||||
session["keys"] = license_keys
|
|
||||||
|
|
||||||
if self.vaults and session["keys"]:
|
if self.vaults and session["keys"]:
|
||||||
key_dict = {UUID(hex=key["kid"]): key["key"] for key in session["keys"] if key["type"] == "CONTENT"}
|
key_dict = {}
|
||||||
|
for key in session["keys"]:
|
||||||
|
if key["type"] == "CONTENT":
|
||||||
|
try:
|
||||||
|
clean_kid = key["kid"].replace("-", "")
|
||||||
|
if len(clean_kid) == 32:
|
||||||
|
kid_uuid = UUID(hex=clean_kid)
|
||||||
|
else:
|
||||||
|
kid_uuid = UUID(hex=clean_kid.ljust(32, "0"))
|
||||||
|
key_dict[kid_uuid] = key["key"]
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
if key_dict:
|
||||||
self.vaults.add_keys(key_dict)
|
self.vaults.add_keys(key_dict)
|
||||||
|
|
||||||
def get_keys(self, session_id: bytes, type_: Optional[str] = None) -> List[Key]:
|
def get_keys(self, session_id: bytes, type_: Optional[str] = None) -> List[Key]:
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ class Track:
|
|||||||
for drm in self.drm:
|
for drm in self.drm:
|
||||||
if isinstance(drm, PlayReady):
|
if isinstance(drm, PlayReady):
|
||||||
return drm
|
return drm
|
||||||
elif hasattr(cdm, 'is_playready'):
|
elif hasattr(cdm, "is_playready"):
|
||||||
if cdm.is_playready:
|
if cdm.is_playready:
|
||||||
for drm in self.drm:
|
for drm in self.drm:
|
||||||
if isinstance(drm, PlayReady):
|
if isinstance(drm, PlayReady):
|
||||||
@@ -567,8 +567,7 @@ class Track:
|
|||||||
output_path = original_path.with_stem(f"{original_path.stem}_repack")
|
output_path = original_path.with_stem(f"{original_path.stem}_repack")
|
||||||
|
|
||||||
def _ffmpeg(extra_args: list[str] = None):
|
def _ffmpeg(extra_args: list[str] = None):
|
||||||
subprocess.run(
|
args = [
|
||||||
[
|
|
||||||
binaries.FFMPEG,
|
binaries.FFMPEG,
|
||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
@@ -576,6 +575,24 @@ class Track:
|
|||||||
"-i",
|
"-i",
|
||||||
original_path,
|
original_path,
|
||||||
*(extra_args or []),
|
*(extra_args or []),
|
||||||
|
]
|
||||||
|
|
||||||
|
if hasattr(self, "data") and self.data.get("audio_language"):
|
||||||
|
audio_lang = self.data["audio_language"]
|
||||||
|
audio_name = self.data.get("audio_language_name", audio_lang)
|
||||||
|
args.extend(
|
||||||
|
[
|
||||||
|
"-metadata:s:a:0",
|
||||||
|
f"language={audio_lang}",
|
||||||
|
"-metadata:s:a:0",
|
||||||
|
f"title={audio_name}",
|
||||||
|
"-metadata:s:a:0",
|
||||||
|
f"handler_name={audio_name}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
args.extend(
|
||||||
|
[
|
||||||
# Following are very important!
|
# Following are very important!
|
||||||
"-map_metadata",
|
"-map_metadata",
|
||||||
"-1", # don't transfer metadata to output file
|
"-1", # don't transfer metadata to output file
|
||||||
@@ -584,7 +601,11 @@ class Track:
|
|||||||
"-codec",
|
"-codec",
|
||||||
"copy",
|
"copy",
|
||||||
str(output_path),
|
str(output_path),
|
||||||
],
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
args,
|
||||||
check=True,
|
check=True,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
|
|||||||
@@ -305,7 +305,14 @@ class Tracks:
|
|||||||
)
|
)
|
||||||
return selected
|
return selected
|
||||||
|
|
||||||
def mux(self, title: str, delete: bool = True, progress: Optional[partial] = None) -> tuple[Path, int, list[str]]:
|
def mux(
|
||||||
|
self,
|
||||||
|
title: str,
|
||||||
|
delete: bool = True,
|
||||||
|
progress: Optional[partial] = None,
|
||||||
|
audio_expected: bool = True,
|
||||||
|
title_language: Optional[Language] = None,
|
||||||
|
) -> tuple[Path, int, list[str]]:
|
||||||
"""
|
"""
|
||||||
Multiplex all the Tracks into a Matroska Container file.
|
Multiplex all the Tracks into a Matroska Container file.
|
||||||
|
|
||||||
@@ -315,7 +322,28 @@ class Tracks:
|
|||||||
delete: Delete all track files after multiplexing.
|
delete: Delete all track files after multiplexing.
|
||||||
progress: Update a rich progress bar via `completed=...`. This must be the
|
progress: Update a rich progress bar via `completed=...`. This must be the
|
||||||
progress object's update() func, pre-set with task id via functools.partial.
|
progress object's update() func, pre-set with task id via functools.partial.
|
||||||
|
audio_expected: Whether audio is expected in the output. Used to determine
|
||||||
|
if embedded audio metadata should be added.
|
||||||
|
title_language: The title's intended language. Used to select the best video track
|
||||||
|
for audio metadata when multiple video tracks exist.
|
||||||
"""
|
"""
|
||||||
|
if self.videos and not self.audio and audio_expected:
|
||||||
|
video_track = None
|
||||||
|
if title_language:
|
||||||
|
video_track = next((v for v in self.videos if v.language == title_language), None)
|
||||||
|
if not video_track:
|
||||||
|
video_track = next((v for v in self.videos if v.is_original_lang), None)
|
||||||
|
|
||||||
|
video_track = video_track or self.videos[0]
|
||||||
|
if video_track.language.is_valid():
|
||||||
|
lang_code = str(video_track.language)
|
||||||
|
lang_name = video_track.language.display_name()
|
||||||
|
|
||||||
|
for video in self.videos:
|
||||||
|
video.needs_repack = True
|
||||||
|
video.data["audio_language"] = lang_code
|
||||||
|
video.data["audio_language_name"] = lang_name
|
||||||
|
|
||||||
if not binaries.MKVToolNix:
|
if not binaries.MKVToolNix:
|
||||||
raise RuntimeError("MKVToolNix (mkvmerge) is required for muxing but was not found")
|
raise RuntimeError("MKVToolNix (mkvmerge) is required for muxing but was not found")
|
||||||
|
|
||||||
@@ -332,12 +360,20 @@ class Tracks:
|
|||||||
raise ValueError("Video Track must be downloaded before muxing...")
|
raise ValueError("Video Track must be downloaded before muxing...")
|
||||||
events.emit(events.Types.TRACK_MULTIPLEX, track=vt)
|
events.emit(events.Types.TRACK_MULTIPLEX, track=vt)
|
||||||
|
|
||||||
|
is_default = False
|
||||||
|
if title_language:
|
||||||
|
is_default = vt.language == title_language
|
||||||
|
if not any(v.language == title_language for v in self.videos):
|
||||||
|
is_default = vt.is_original_lang or i == 0
|
||||||
|
else:
|
||||||
|
is_default = i == 0
|
||||||
|
|
||||||
# Prepare base arguments
|
# Prepare base arguments
|
||||||
video_args = [
|
video_args = [
|
||||||
"--language",
|
"--language",
|
||||||
f"0:{vt.language}",
|
f"0:{vt.language}",
|
||||||
"--default-track",
|
"--default-track",
|
||||||
f"0:{i == 0}",
|
f"0:{is_default}",
|
||||||
"--original-flag",
|
"--original-flag",
|
||||||
f"0:{vt.is_original_lang}",
|
f"0:{vt.is_original_lang}",
|
||||||
"--compression",
|
"--compression",
|
||||||
@@ -363,6 +399,18 @@ class Tracks:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if hasattr(vt, "data") and vt.data.get("audio_language"):
|
||||||
|
audio_lang = vt.data["audio_language"]
|
||||||
|
audio_name = vt.data.get("audio_language_name", audio_lang)
|
||||||
|
video_args.extend(
|
||||||
|
[
|
||||||
|
"--language",
|
||||||
|
f"1:{audio_lang}",
|
||||||
|
"--track-name",
|
||||||
|
f"1:{audio_name}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
cl.extend(video_args + ["(", str(vt.path), ")"])
|
cl.extend(video_args + ["(", str(vt.path), ")"])
|
||||||
|
|
||||||
for i, at in enumerate(self.audio):
|
for i, at in enumerate(self.audio):
|
||||||
|
|||||||
@@ -28,26 +28,33 @@ class MySQL(Vault):
|
|||||||
raise PermissionError(f"MySQL vault {self.slug} has no SELECT permission.")
|
raise PermissionError(f"MySQL vault {self.slug} has no SELECT permission.")
|
||||||
|
|
||||||
def get_key(self, kid: Union[UUID, str], service: str) -> Optional[str]:
|
def get_key(self, kid: Union[UUID, str], service: str) -> Optional[str]:
|
||||||
if not self.has_table(service):
|
|
||||||
# no table, no key, simple
|
|
||||||
return None
|
|
||||||
|
|
||||||
if isinstance(kid, UUID):
|
if isinstance(kid, UUID):
|
||||||
kid = kid.hex
|
kid = kid.hex
|
||||||
|
|
||||||
|
service_variants = [service]
|
||||||
|
if service != service.lower():
|
||||||
|
service_variants.append(service.lower())
|
||||||
|
if service != service.upper():
|
||||||
|
service_variants.append(service.upper())
|
||||||
|
|
||||||
conn = self.conn_factory.get()
|
conn = self.conn_factory.get()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
for service_name in service_variants:
|
||||||
|
if not self.has_table(service_name):
|
||||||
|
continue
|
||||||
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
# TODO: SQL injection risk
|
# TODO: SQL injection risk
|
||||||
f"SELECT `id`, `key_` FROM `{service}` WHERE `kid`=%s AND `key_`!=%s",
|
f"SELECT `id`, `key_` FROM `{service_name}` WHERE `kid`=%s AND `key_`!=%s",
|
||||||
(kid, "0" * 32),
|
(kid, "0" * 32),
|
||||||
)
|
)
|
||||||
cek = cursor.fetchone()
|
cek = cursor.fetchone()
|
||||||
if not cek:
|
if cek:
|
||||||
return None
|
|
||||||
return cek["key_"]
|
return cek["key_"]
|
||||||
|
|
||||||
|
return None
|
||||||
finally:
|
finally:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
|
|||||||
@@ -19,22 +19,30 @@ class SQLite(Vault):
|
|||||||
self.conn_factory = ConnectionFactory(self.path)
|
self.conn_factory = ConnectionFactory(self.path)
|
||||||
|
|
||||||
def get_key(self, kid: Union[UUID, str], service: str) -> Optional[str]:
|
def get_key(self, kid: Union[UUID, str], service: str) -> Optional[str]:
|
||||||
if not self.has_table(service):
|
|
||||||
# no table, no key, simple
|
|
||||||
return None
|
|
||||||
|
|
||||||
if isinstance(kid, UUID):
|
if isinstance(kid, UUID):
|
||||||
kid = kid.hex
|
kid = kid.hex
|
||||||
|
|
||||||
conn = self.conn_factory.get()
|
conn = self.conn_factory.get()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Try both the original service name and lowercase version to handle case sensitivity issues
|
||||||
|
service_variants = [service]
|
||||||
|
if service != service.lower():
|
||||||
|
service_variants.append(service.lower())
|
||||||
|
if service != service.upper():
|
||||||
|
service_variants.append(service.upper())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor.execute(f"SELECT `id`, `key_` FROM `{service}` WHERE `kid`=? AND `key_`!=?", (kid, "0" * 32))
|
for service_name in service_variants:
|
||||||
|
if not self.has_table(service_name):
|
||||||
|
continue
|
||||||
|
|
||||||
|
cursor.execute(f"SELECT `id`, `key_` FROM `{service_name}` WHERE `kid`=? AND `key_`!=?", (kid, "0" * 32))
|
||||||
cek = cursor.fetchone()
|
cek = cursor.fetchone()
|
||||||
if not cek:
|
if cek:
|
||||||
return None
|
|
||||||
return cek[1]
|
return cek[1]
|
||||||
|
|
||||||
|
return None
|
||||||
finally:
|
finally:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user