forked from FairTrade/unshackle-services
Added GLA
This commit is contained in:
parent
9140d26d31
commit
7013de5c01
471
GLA/__init__.py
Normal file
471
GLA/__init__.py
Normal file
@ -0,0 +1,471 @@
|
|||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from collections.abc import Generator
|
||||||
|
from datetime import datetime
|
||||||
|
from http.cookiejar import CookieJar
|
||||||
|
from typing import Optional, Union
|
||||||
|
from urllib.parse import urljoin, parse_qs, urlparse
|
||||||
|
|
||||||
|
import click
|
||||||
|
from langcodes import Language
|
||||||
|
|
||||||
|
from unshackle.core.constants import AnyTrack
|
||||||
|
from unshackle.core.credential import Credential
|
||||||
|
from unshackle.core.manifests import DASH, HLS
|
||||||
|
from unshackle.core.search_result import SearchResult
|
||||||
|
from unshackle.core.service import Service
|
||||||
|
from unshackle.core.titles import Episode, Movie, Movies, Series, Title_T, Titles_T
|
||||||
|
from unshackle.core.tracks import Chapter, Subtitle, Tracks, Video
|
||||||
|
|
||||||
|
|
||||||
|
class GLA(Service):
|
||||||
|
"""
|
||||||
|
Service code for gagaoolala.com
|
||||||
|
Version: 1.0.1
|
||||||
|
|
||||||
|
Authorization: Email/Password or Cookies (PHPSESSID)
|
||||||
|
Security: FHD@L3 (Widevine/PlayReady DRM via ExpressPlay)
|
||||||
|
|
||||||
|
Use full URL: https://www.gagaoolala.com/en/videos/6184/candy-2026
|
||||||
|
Or title ID: 6184 (slug will be fetched from page if needed)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Updated regex to optionally capture slug
|
||||||
|
TITLE_RE = r"^(?:https?://(?:www\.)?gagaoolala\.com/(?:en/)?videos/)?(?P<title_id>\d+)(?:/(?P<slug>[^/?#]+))?"
|
||||||
|
GEOFENCE = ()
|
||||||
|
NO_SUBTITLES = False
|
||||||
|
|
||||||
|
VIDEO_RANGE_MAP = {
|
||||||
|
"SDR": "sdr",
|
||||||
|
"HDR10": "hdr10",
|
||||||
|
"DV": "dolby_vision",
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@click.command(name="GLA", short_help="https://www.gagaoolala.com")
|
||||||
|
@click.argument("title", type=str)
|
||||||
|
@click.option("-m", "--movie", is_flag=True, default=False, help="Specify if it's a movie")
|
||||||
|
@click.option("-d", "--device", type=str, default="firefox_linux", help="Select device profile")
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, **kwargs):
|
||||||
|
return GLA(ctx, **kwargs)
|
||||||
|
|
||||||
|
def __init__(self, ctx, title, movie, device, email=None, password=None):
|
||||||
|
super().__init__(ctx)
|
||||||
|
|
||||||
|
self.title = title
|
||||||
|
self.movie = movie
|
||||||
|
self.device = device
|
||||||
|
self.email = email
|
||||||
|
self.password = password
|
||||||
|
self.cdm = ctx.obj.cdm
|
||||||
|
|
||||||
|
# Override codec/range for L3 CDM limitations
|
||||||
|
if self.cdm and self.cdm.security_level == 3:
|
||||||
|
self.track_request.codecs = [Video.Codec.AVC]
|
||||||
|
self.track_request.ranges = [Video.Range.SDR]
|
||||||
|
|
||||||
|
if self.config is None:
|
||||||
|
raise Exception("Config is missing!")
|
||||||
|
|
||||||
|
self.profile = ctx.parent.params.get("profile") or "default"
|
||||||
|
self.user_id = None
|
||||||
|
self.license_data = {}
|
||||||
|
self.slug = None # Store slug for API calls
|
||||||
|
|
||||||
|
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
|
||||||
|
super().authenticate(cookies, credential)
|
||||||
|
|
||||||
|
if cookies:
|
||||||
|
self.session.cookies.update(cookies)
|
||||||
|
for cookie in cookies:
|
||||||
|
if cookie.name == "gli":
|
||||||
|
self.user_id = cookie.value
|
||||||
|
break
|
||||||
|
return
|
||||||
|
|
||||||
|
if not credential or not credential.username or not credential.password:
|
||||||
|
raise EnvironmentError("Service requires Cookies or Credential (email/password) for Authentication.")
|
||||||
|
|
||||||
|
login_url = "https://www.gagaoolala.com/en/user/login"
|
||||||
|
login_data = {
|
||||||
|
"email": credential.username,
|
||||||
|
"passwd": credential.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": self.config["client"][self.device]["user_agent"],
|
||||||
|
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"Origin": "https://www.gagaoolala.com",
|
||||||
|
"Referer": login_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.session.post(login_url, data=login_data, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if not result.get("success"):
|
||||||
|
error_msg = result.get("msg") or result.get("data", {}).get("msg") or "Unknown error"
|
||||||
|
raise AuthenticationError(f"Login failed: {error_msg}")
|
||||||
|
|
||||||
|
self.user_id = result.get("data", {}).get("user_line_uid")
|
||||||
|
|
||||||
|
def search(self) -> Generator[SearchResult, None, None]:
|
||||||
|
search_url = "https://www.gagaoolala.com/en/search"
|
||||||
|
params = {"q": self.title}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": self.config["client"][self.device]["user_agent"],
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.session.get(search_url, params=params, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
content_type = response.headers.get("Content-Type", "")
|
||||||
|
if "application/json" in content_type:
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
data = None
|
||||||
|
else:
|
||||||
|
data = None
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
html = response.text
|
||||||
|
|
||||||
|
json_ld_match = re.search(
|
||||||
|
r'<script[^>]+type=["\']application/ld\+json["\'][^>]*>\s*({.*?"@context".*?})\s*</script>',
|
||||||
|
html,
|
||||||
|
re.DOTALL | re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
if json_ld_match:
|
||||||
|
json_str = json_ld_match.group(1)
|
||||||
|
json_str = json_str.replace(r'\/', '/').replace(r'\"', '"')
|
||||||
|
try:
|
||||||
|
data = json.loads(json_str)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
self.log.debug(f"Failed to parse JSON-LD: {e}")
|
||||||
|
data = None
|
||||||
|
else:
|
||||||
|
fallback_match = re.search(
|
||||||
|
r'(\{[^{}]*"@context"[^{}]*"itemListElement"[^{}]*\[\s*\{[^{}]*"url"[^{}]*\][^{}]*\})',
|
||||||
|
html,
|
||||||
|
re.DOTALL
|
||||||
|
)
|
||||||
|
if fallback_match:
|
||||||
|
try:
|
||||||
|
data = json.loads(fallback_match.group(1))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
data = None
|
||||||
|
|
||||||
|
if not data or "itemListElement" not in data:
|
||||||
|
self.log.warning(f"No search results found for '{self.title}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in data["itemListElement"]:
|
||||||
|
url = item.get("url", "")
|
||||||
|
if not url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = re.match(self.TITLE_RE, url)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
|
||||||
|
title_id = match.group("title_id")
|
||||||
|
slug = match.group("slug")
|
||||||
|
|
||||||
|
# Extract title name from slug or URL
|
||||||
|
title_name = slug if slug else url.rstrip("/").split("/")[-1]
|
||||||
|
if "-" in title_name:
|
||||||
|
parts = title_name.rsplit("-", 1)
|
||||||
|
# Remove year suffix if present (e.g., candy-2026 -> candy)
|
||||||
|
if parts[-1].isdigit() and len(parts[-1]) == 4:
|
||||||
|
title_name = parts[0]
|
||||||
|
title_name = title_name.replace("-", " ").title()
|
||||||
|
|
||||||
|
# Detect series vs movie
|
||||||
|
is_series = bool(slug and ("-e" in slug or slug.endswith("-e01")))
|
||||||
|
|
||||||
|
yield SearchResult(
|
||||||
|
id_=title_id,
|
||||||
|
title=title_name,
|
||||||
|
label="SERIES" if is_series else "MOVIE",
|
||||||
|
url=url,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _clean_title(self, raw_title: str, slug: Optional[str] = None) -> str:
|
||||||
|
"""Clean up page titles by removing SEO/marketing suffixes."""
|
||||||
|
title = re.sub(r'\s*\|\s*GagaOOLala\s*$', '', raw_title).strip()
|
||||||
|
|
||||||
|
seo_patterns = [
|
||||||
|
r'\s*-\s*Watch\s+Online.*$',
|
||||||
|
r'\s*-\s*Find\s+Your\s+Story.*$',
|
||||||
|
r'\s*-\s*Watch\s+BL\s+Movies.*$',
|
||||||
|
r'\s*-\s*Stream\s+Online.*$',
|
||||||
|
r'\s*-\s*Free\s+Streaming.*$',
|
||||||
|
r'\s*-\s*GagaOOLala.*$',
|
||||||
|
]
|
||||||
|
for pattern in seo_patterns:
|
||||||
|
title = re.sub(pattern, '', title, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
title = re.sub(r'\s*-\s*$', '', title).strip()
|
||||||
|
|
||||||
|
if slug:
|
||||||
|
slug_title = slug.replace('-', ' ').title()
|
||||||
|
year_match = re.search(r'(\d{4})$', slug)
|
||||||
|
if year_match:
|
||||||
|
year = year_match.group(1)
|
||||||
|
slug_title = re.sub(r'\s*\d{4}\s*$', '', slug_title).strip()
|
||||||
|
candidate = f"{slug_title} ({year})"
|
||||||
|
if len(candidate) < len(title) or title.lower().startswith(slug_title.lower()):
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
return title if title else f"Title {self.title}"
|
||||||
|
|
||||||
|
def get_titles(self) -> Titles_T:
|
||||||
|
match = re.match(self.TITLE_RE, self.title)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Could not parse title ID from: {self.title}")
|
||||||
|
|
||||||
|
title_id = match.group("title_id")
|
||||||
|
self.slug = match.group("slug")
|
||||||
|
|
||||||
|
video_url = f"https://www.gagaoolala.com/en/videos/{title_id}"
|
||||||
|
if self.slug:
|
||||||
|
video_url += f"/{self.slug}"
|
||||||
|
|
||||||
|
response = self.session.get(video_url)
|
||||||
|
|
||||||
|
if response.status_code == 404 and self.slug:
|
||||||
|
self.log.warning(f"URL with slug returned 404, trying without slug")
|
||||||
|
video_url = f"https://www.gagaoolala.com/en/videos/{title_id}"
|
||||||
|
response = self.session.get(video_url)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
episodes_match = re.search(r'var\s+videoEpisodes\s*=\s*(\[.*?\]);\s*var\s+videoSeasons', response.text, re.DOTALL)
|
||||||
|
|
||||||
|
if episodes_match:
|
||||||
|
episodes_data = json.loads(episodes_match.group(1))
|
||||||
|
series_episodes = [ep for ep in episodes_data if ep.get("is_series")]
|
||||||
|
|
||||||
|
if series_episodes:
|
||||||
|
first_name = series_episodes[0].get("name", "")
|
||||||
|
base_title = re.sub(r'\s*Episode\s*\d+.*$', '', first_name).strip()
|
||||||
|
if not base_title and self.slug:
|
||||||
|
base_title = self._clean_title(self.slug.replace('-', ' ').title(), None)
|
||||||
|
if not base_title:
|
||||||
|
base_title = f"Series {title_id}"
|
||||||
|
|
||||||
|
episodes = []
|
||||||
|
for ep in series_episodes:
|
||||||
|
ep_slug = ep.get("slug", f"{self.slug}-e{ep.get('episode', 1)}" if self.slug else None)
|
||||||
|
episodes.append(
|
||||||
|
Episode(
|
||||||
|
id_=str(ep["id"]),
|
||||||
|
service=self.__class__,
|
||||||
|
title=base_title,
|
||||||
|
season=ep.get("season", 1),
|
||||||
|
number=ep.get("episode", 1),
|
||||||
|
name=ep.get("name", f"Episode {ep.get('episode', 1)}"),
|
||||||
|
description=None,
|
||||||
|
year=None,
|
||||||
|
language=Language.get("en"),
|
||||||
|
data={**ep, "slug": ep_slug, "parent_slug": self.slug},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return Series(episodes)
|
||||||
|
|
||||||
|
title_match = re.search(r'<title>([^<]+)</title>', response.text)
|
||||||
|
raw_title = title_match.group(1) if title_match else (self.slug or f"Movie {title_id}")
|
||||||
|
movie_title = self._clean_title(raw_title, self.slug)
|
||||||
|
|
||||||
|
year = None
|
||||||
|
year_match = re.search(r'\((\d{4})\)\s*$', movie_title)
|
||||||
|
if year_match:
|
||||||
|
year = int(year_match.group(1))
|
||||||
|
movie_title = re.sub(r'\s*\(\d{4}\)\s*$', '', movie_title).strip()
|
||||||
|
elif self.slug:
|
||||||
|
slug_year = re.search(r'(\d{4})$', self.slug)
|
||||||
|
if slug_year:
|
||||||
|
year = int(slug_year.group(1))
|
||||||
|
|
||||||
|
return Movies(
|
||||||
|
[
|
||||||
|
Movie(
|
||||||
|
id_=title_id,
|
||||||
|
service=self.__class__,
|
||||||
|
name=movie_title,
|
||||||
|
description=None,
|
||||||
|
year=year,
|
||||||
|
language=Language.get("en"),
|
||||||
|
data={"url": video_url, "slug": self.slug},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_tracks(self, title: Title_T) -> Tracks:
|
||||||
|
def _fetch_variant(
|
||||||
|
title: Title_T,
|
||||||
|
codec: Optional[Video.Codec],
|
||||||
|
range_: Video.Range,
|
||||||
|
) -> Tracks:
|
||||||
|
vcodec_str = "H265" if codec == Video.Codec.HEVC else "H264"
|
||||||
|
range_str = range_.name
|
||||||
|
video_format = self.VIDEO_RANGE_MAP.get(range_str, "sdr")
|
||||||
|
|
||||||
|
tracks = self._fetch_manifest(title)
|
||||||
|
|
||||||
|
if codec:
|
||||||
|
tracks.videos = [v for v in tracks.videos if v.codec == codec]
|
||||||
|
if range_ != Video.Range.SDR:
|
||||||
|
tracks.videos = [v for v in tracks.videos if v.range == range_]
|
||||||
|
|
||||||
|
if not tracks.videos:
|
||||||
|
raise ValueError(f"No tracks available for {codec} {range_}")
|
||||||
|
|
||||||
|
return tracks
|
||||||
|
|
||||||
|
tracks = self._get_tracks_for_variants(title, _fetch_variant)
|
||||||
|
return tracks
|
||||||
|
|
||||||
|
def get_tracks(self, title: Title_T) -> Tracks:
|
||||||
|
def _fetch_variant(
|
||||||
|
title: Title_T,
|
||||||
|
codec: Optional[Video.Codec],
|
||||||
|
range_: Video.Range,
|
||||||
|
) -> Tracks:
|
||||||
|
vcodec_str = "H265" if codec == Video.Codec.HEVC else "H264"
|
||||||
|
range_str = range_.name
|
||||||
|
video_format = self.VIDEO_RANGE_MAP.get(range_str, "sdr")
|
||||||
|
|
||||||
|
tracks = self._fetch_manifest(title)
|
||||||
|
|
||||||
|
if codec:
|
||||||
|
tracks.videos = [v for v in tracks.videos if v.codec == codec]
|
||||||
|
if range_ != Video.Range.SDR:
|
||||||
|
tracks.videos = [v for v in tracks.videos if v.range == range_]
|
||||||
|
|
||||||
|
if not tracks.videos:
|
||||||
|
raise ValueError(f"No tracks available for {codec} {range_}")
|
||||||
|
|
||||||
|
return tracks
|
||||||
|
|
||||||
|
return self._get_tracks_for_variants(title, _fetch_variant)
|
||||||
|
|
||||||
|
def _fetch_manifest(self, title: Title_T) -> Tracks:
|
||||||
|
timestamp = int(time.time())
|
||||||
|
|
||||||
|
slug = title.data.get("slug") if isinstance(title.data, dict) else None
|
||||||
|
if not slug:
|
||||||
|
slug = title.data.get("parent_slug") if isinstance(title.data, dict) else self.slug
|
||||||
|
|
||||||
|
if not slug:
|
||||||
|
match = re.match(self.TITLE_RE, self.title)
|
||||||
|
if match:
|
||||||
|
slug = match.group("slug")
|
||||||
|
|
||||||
|
if slug:
|
||||||
|
play_url = f"https://www.gagaoolala.com/api/v1.0/en/videos/{title.id}/{slug}/play"
|
||||||
|
else:
|
||||||
|
play_url = f"https://www.gagaoolala.com/api/v1.0/en/videos/{title.id}/play"
|
||||||
|
self.log.warning(f"No slug available, attempting play request without slug: {play_url}")
|
||||||
|
|
||||||
|
params = {"t": timestamp}
|
||||||
|
|
||||||
|
response = self.session.get(play_url, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
playback = response.json()
|
||||||
|
if not playback.get("success"):
|
||||||
|
raise ValueError(f"Failed to get playback info: {playback}")
|
||||||
|
|
||||||
|
data = playback["data"]
|
||||||
|
|
||||||
|
drm_info = data.get("drm")
|
||||||
|
if drm_info:
|
||||||
|
self.license_data = {
|
||||||
|
"widevine": drm_info.get("widevine", {}).get("LA_URL"),
|
||||||
|
"playready": drm_info.get("playready", {}).get("LA_URL"),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self.license_data = {}
|
||||||
|
|
||||||
|
manifest_url = data.get("dash") or data.get("m3u8")
|
||||||
|
if not manifest_url:
|
||||||
|
raise ValueError("No manifest URL found in playback response")
|
||||||
|
|
||||||
|
if ".mpd" in manifest_url:
|
||||||
|
tracks = DASH.from_url(url=manifest_url, session=self.session).to_tracks(language=title.language)
|
||||||
|
elif ".m3u8" in manifest_url:
|
||||||
|
tracks = HLS.from_url(url=manifest_url, session=self.session).to_tracks(language=title.language)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported manifest format: {manifest_url}")
|
||||||
|
|
||||||
|
for video in tracks.videos:
|
||||||
|
if video.codec == Video.Codec.HEVC and video.profile and "Main10" in str(video.profile):
|
||||||
|
video.range = Video.Range.HDR10
|
||||||
|
else:
|
||||||
|
video.range = Video.Range.SDR
|
||||||
|
|
||||||
|
return tracks
|
||||||
|
|
||||||
|
def get_chapters(self, title: Title_T) -> list[Chapter]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_widevine_service_certificate(self, **_: any) -> str:
|
||||||
|
return self.config.get("certificate", "")
|
||||||
|
|
||||||
|
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
|
||||||
|
if not self.license_data.get("widevine"):
|
||||||
|
raise ValueError("Widevine license URL not available for this title")
|
||||||
|
|
||||||
|
license_url = self.license_data["widevine"]
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": self.config["client"][self.device].get("license_user_agent",
|
||||||
|
self.config["client"][self.device]["user_agent"]),
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.session.post(
|
||||||
|
url=license_url,
|
||||||
|
data=challenge,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
def get_playready_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
|
||||||
|
if not self.license_data.get("playready"):
|
||||||
|
raise ValueError("PlayReady license URL not available for this title")
|
||||||
|
|
||||||
|
license_url = self.license_data["playready"]
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": self.config["client"][self.device].get("license_user_agent",
|
||||||
|
self.config["client"][self.device]["user_agent"]),
|
||||||
|
"Content-Type": "text/xml",
|
||||||
|
"SOAPAction": "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.session.post(
|
||||||
|
url=license_url,
|
||||||
|
data=challenge,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.content
|
||||||
16
GLA/config.yaml
Normal file
16
GLA/config.yaml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# config.yaml for GLA (GagaOOLala)
|
||||||
|
endpoints:
|
||||||
|
login: https://www.gagaoolala.com/en/user/login
|
||||||
|
play: https://www.gagaoolala.com/api/v1.0/en/videos/{title_id}/{slug}/play
|
||||||
|
search: https://www.gagaoolala.com/en/search
|
||||||
|
|
||||||
|
client:
|
||||||
|
firefox_linux:
|
||||||
|
user_agent: "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0"
|
||||||
|
license_user_agent: "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0"
|
||||||
|
android_tv:
|
||||||
|
user_agent: "Mozilla/5.0 (Linux; Android 10; Android TV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Safari/537.36"
|
||||||
|
license_user_agent: "ExoPlayerLib/2.18.1"
|
||||||
|
windows_chrome:
|
||||||
|
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
license_user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||||
@ -39,6 +39,8 @@
|
|||||||
- Music needs to be fixed since the output is a mp4 instead of m4a
|
- Music needs to be fixed since the output is a mp4 instead of m4a
|
||||||
13. SHUD:
|
13. SHUD:
|
||||||
- PlayReady needed
|
- PlayReady needed
|
||||||
|
14. GLA:
|
||||||
|
- Subs sometimes broken (it's on there side)
|
||||||
|
|
||||||
- Acknowledgment
|
- Acknowledgment
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user