mirror of
https://github.com/vinefeeder/TwinVine.git
synced 2026-07-15 18:10:04 +02:00
TPTV and updates
This commit is contained in:
@@ -23,8 +23,8 @@ uv run unshackle dl -vl all -al orig -sl ko,en,ja -q 1080,2160 -v h.264,h.265 -r
|
|||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
- To enable the web refresh token-based login method, please comment out or delete the DSNP section under credentials in `unshackle.yaml`.
|
- To enable the web refresh token-based login method, please comment out or delete the DSNP section under credentials in `envied.yaml`.
|
||||||
웹 리프레시 토큰 기반 로그인 방식을 활성화하려면 `unshackle.yaml`의 credentials에서 DSNP부분을 주석 처리하거나 삭제하세요.
|
웹 리프레시 토큰 기반 로그인 방식을 활성화하려면 `envied.yaml`의 credentials에서 DSNP부분을 주석 처리하거나 삭제하세요.
|
||||||
|
|
||||||
```
|
```
|
||||||
credentials:
|
credentials:
|
||||||
@@ -32,8 +32,8 @@ uv run unshackle dl -vl all -al orig -sl ko,en,ja -q 1080,2160 -v h.264,h.265 -r
|
|||||||
# DSNP: example@example.com:example
|
# DSNP: example@example.com:example
|
||||||
```
|
```
|
||||||
|
|
||||||
- Configure user settings within the `unshackle.yaml` file.
|
- Configure user settings within the `envied.yaml` file.
|
||||||
사용자 설정은 `unshackle.yaml`에서 다음과 같이 사용하세요.
|
사용자 설정은 `envied.yaml`에서 다음과 같이 사용하세요.
|
||||||
|
|
||||||
```
|
```
|
||||||
services:
|
services:
|
||||||
@@ -66,8 +66,8 @@ uv run unshackle dl -vl all -al orig -sl ko,en,ja -q 1080,2160 -v h.264,h.265 -r
|
|||||||
# manifest_log: "info"
|
# manifest_log: "info"
|
||||||
```
|
```
|
||||||
|
|
||||||
- To enable the tier_unlimits command by default, add the following to `unshackle.yaml`.
|
- To enable the tier_unlimits command by default, add the following to `envied.yaml`.
|
||||||
tier_unlimits 명령을 기본값으로 활성화하려면 `unshackle.yaml`에 다음을 추가하세요.
|
tier_unlimits 명령을 기본값으로 활성화하려면 `envied.yaml`에 다음을 추가하세요.
|
||||||
```
|
```
|
||||||
dl:
|
dl:
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
## DO NOT EDIT THIS FILE
|
## DO NOT EDIT THIS FILE
|
||||||
# 해당 config 파일은 개발자 외에는 수정하지 마세요.
|
# 해당 config 파일은 개발자 외에는 수정하지 마세요.
|
||||||
# This configuration file should not be modified by anyone other than developers.
|
# This configuration file should not be modified by anyone other than developers.
|
||||||
# 사용자 환경설정은 반드시 "unshackle.yaml"에서만 사용하세요.
|
# 사용자 환경설정은 반드시 "envied.yaml"에서만 사용하세요.
|
||||||
# User configuration must be performed only in "unshackle.yaml".
|
# User configuration must be performed only in "envied.yaml".
|
||||||
|
|
||||||
certificate: |
|
certificate: |
|
||||||
CAUSugUKtAIIAxIQbj3s4jO5oUyWjDWqjfr9WRjA2afZBSKOAjCCAQoCggEBALhKWfnyA+FGn5P3tl6ffDjoGq2Oq86hKGl6aZIaGaF7XHPO5mIk7Q35ml
|
CAUSugUKtAIIAxIQbj3s4jO5oUyWjDWqjfr9WRjA2afZBSKOAjCCAQoCggEBALhKWfnyA+FGn5P3tl6ffDjoGq2Oq86hKGl6aZIaGaF7XHPO5mIk7Q35ml
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ uv run unshackle dl -vl all -al orig -sl ko,en,ja -q 1080,2160 -v h.264,h.265 -r
|
|||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
- To enable the web refresh token-based login method, please comment out or delete the DSNP section under credentials in `unshackle.yaml`.
|
- To enable the web refresh token-based login method, please comment out or delete the DSNP section under credentials in `envied.yaml`.
|
||||||
웹 리프레시 토큰 기반 로그인 방식을 활성화하려면 `unshackle.yaml`의 credentials에서 DSNP부분을 주석 처리하거나 삭제하세요.
|
웹 리프레시 토큰 기반 로그인 방식을 활성화하려면 `envied.yaml`의 credentials에서 DSNP부분을 주석 처리하거나 삭제하세요.
|
||||||
|
|
||||||
```
|
```
|
||||||
credentials:
|
credentials:
|
||||||
@@ -32,8 +32,8 @@ uv run unshackle dl -vl all -al orig -sl ko,en,ja -q 1080,2160 -v h.264,h.265 -r
|
|||||||
# DSNP: example@example.com:example
|
# DSNP: example@example.com:example
|
||||||
```
|
```
|
||||||
|
|
||||||
- Configure user settings within the `unshackle.yaml` file.
|
- Configure user settings within the `envied.yaml` file.
|
||||||
사용자 설정은 `unshackle.yaml`에서 다음과 같이 사용하세요.
|
사용자 설정은 `envied.yaml`에서 다음과 같이 사용하세요.
|
||||||
|
|
||||||
```
|
```
|
||||||
services:
|
services:
|
||||||
@@ -66,8 +66,8 @@ uv run unshackle dl -vl all -al orig -sl ko,en,ja -q 1080,2160 -v h.264,h.265 -r
|
|||||||
# manifest_log: "info"
|
# manifest_log: "info"
|
||||||
```
|
```
|
||||||
|
|
||||||
- To enable the tier_unlimits command by default, add the following to `unshackle.yaml`.
|
- To enable the tier_unlimits command by default, add the following to `envied.yaml`.
|
||||||
tier_unlimits 명령을 기본값으로 활성화하려면 `unshackle.yaml`에 다음을 추가하세요.
|
tier_unlimits 명령을 기본값으로 활성화하려면 `envied.yaml`에 다음을 추가하세요.
|
||||||
```
|
```
|
||||||
dl:
|
dl:
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
## DO NOT EDIT THIS FILE
|
## DO NOT EDIT THIS FILE
|
||||||
# 해당 config 파일은 개발자 외에는 수정하지 마세요.
|
# 해당 config 파일은 개발자 외에는 수정하지 마세요.
|
||||||
# This configuration file should not be modified by anyone other than developers.
|
# This configuration file should not be modified by anyone other than developers.
|
||||||
# 사용자 환경설정은 반드시 "unshackle.yaml"에서만 사용하세요.
|
# 사용자 환경설정은 반드시 "envied.yaml"에서만 사용하세요.
|
||||||
# User configuration must be performed only in "unshackle.yaml".
|
# User configuration must be performed only in "envied.yaml".
|
||||||
|
|
||||||
certificate: |
|
certificate: |
|
||||||
CAUSugUKtAIIAxIQbj3s4jO5oUyWjDWqjfr9WRjA2afZBSKOAjCCAQoCggEBALhKWfnyA+FGn5P3tl6ffDjoGq2Oq86hKGl6aZIaGaF7XHPO5mIk7Q35ml
|
CAUSugUKtAIIAxIQbj3s4jO5oUyWjDWqjfr9WRjA2afZBSKOAjCCAQoCggEBALhKWfnyA+FGn5P3tl6ffDjoGq2Oq86hKGl6aZIaGaF7XHPO5mIk7Q35ml
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ from envied.core.service import Service
|
|||||||
from envied.core.titles import Episode, Movie, Movies, Series
|
from envied.core.titles import Episode, Movie, Movies, Series
|
||||||
from envied.core.tracks import Chapters, Subtitle, Tracks
|
from envied.core.tracks import Chapters, Subtitle, Tracks
|
||||||
import requests
|
import requests
|
||||||
|
import uuid
|
||||||
|
# debugging
|
||||||
|
from rich.console import Console
|
||||||
|
console = Console()
|
||||||
|
|
||||||
def get_widevine_license_url(manifest_str):
|
def get_widevine_license_url(manifest_str):
|
||||||
# Try JSON first
|
# Try JSON first
|
||||||
@@ -40,7 +44,7 @@ class TPTV(Service):
|
|||||||
|
|
||||||
\b
|
\b
|
||||||
version 1.0.4
|
version 1.0.4
|
||||||
Date: June 2025
|
Date: June 2026
|
||||||
Author: A_n_g_e_l_a
|
Author: A_n_g_e_l_a
|
||||||
Authorization: email/password for service in envied.yaml
|
Authorization: email/password for service in envied.yaml
|
||||||
Robustness:
|
Robustness:
|
||||||
@@ -50,23 +54,36 @@ class TPTV(Service):
|
|||||||
Note:
|
Note:
|
||||||
TPTV will not allow the usual -w S01-S04 syntax as TPTV is eclictic in what it serves.
|
TPTV will not allow the usual -w S01-S04 syntax as TPTV is eclictic in what it serves.
|
||||||
Series and episodes carry little meaning on this platform.
|
Series and episodes carry little meaning on this platform.
|
||||||
|
|
||||||
It is not possible to remove S00E00 from the end of a video title - envied insists.
|
It is not possible to remove S00E00 from the end of a video title - envied insists.
|
||||||
|
|
||||||
|
This service is best used with Vinefeeder for its front end.
|
||||||
|
Then use a search term in preference to a url.
|
||||||
|
|
||||||
|
Dealing with COLLECTION:
|
||||||
|
In envied, any url with COLLECTION in capitals will attempt to list the whole.
|
||||||
|
In vinefeeder, COLLECTION is ignored and the descriptive in the url is used as a search term.
|
||||||
|
eg https://tptvencore.co.uk/details/COLLECTION/collection/01KRKTC8WZ7X584YPA5NBFYJSS/the-danziger-collection
|
||||||
|
will be treated as a search for "the danziger collection" in vinefeeder, but will attempt to list the whole collection in envied.
|
||||||
|
Series and episodes are not well defined in TPTV, the collection listing is one way to find the episodes of a series.
|
||||||
|
If you look on ay COLLECTON page at TPTVencore there are tabs showing series listings. Use vinefeeder to search for the series name
|
||||||
|
and to produce a selectable list.
|
||||||
|
|
||||||
|
If COLLECTION name only returns one tab of results then manually from the web-page select each link url.
|
||||||
|
They are of the form https://tptvencore.co.uk/playback/item/6386553443112
|
||||||
|
|
||||||
|
Some restricted content will not download.
|
||||||
\b
|
\b
|
||||||
Tips:
|
Tips:
|
||||||
Use complete url in all cases.
|
Use complete url in all cases.
|
||||||
SERIES: https://tptvencore.co.uk/collection/1717422888871355373
|
SERIES: https://tptvencore.co.uk/details/COLLECTION/collection/01KRKTC8WZ7X584YPA5NBFYJSS/the-danziger-collection
|
||||||
Note: TPTV do not specify Series and Episodes numbers in any meaningful and organized way.
|
Note: TPTV do not specify Series and Episodes numbers in any meaningful and organized way.
|
||||||
They MAY sometimes be in the program title, but often incomplete.
|
They MAY sometimes be in the program title, but often incomplete.
|
||||||
FILM: https://tptvencore.co.uk/product/the-importance-of-being-earnest-6290333578001
|
FILM: https://tptvencore.co.uk/details/VIDEO/item/6390689314112/the-manster
|
||||||
EPISODE: https://tptvencore.co.uk/product/sherlock-holmes---the-case-of-greystone-inscription-s1-ep16-6282604132001
|
EPISODE: https://tptvencore.co.uk/details/VIDEO/item/6348388311112/maigret-peter-the-lett
|
||||||
|
EPISODE: https://tptvencore.co.uk/playback/item/6386553443112
|
||||||
|
TPTV makes no distinction between films and episodes, so the above is just a convention to help you find what you want.
|
||||||
|
|
||||||
|
|
||||||
\b
|
|
||||||
Examples:
|
|
||||||
SERIES: https://tptvencore.co.uk/collection/1717422888871355373
|
|
||||||
EPISODE: https://tptvencore.co.uk/product/sherlock-holmes---the-case-of-greystone-inscription-s1-ep16-6282604132001
|
|
||||||
FILM: https://tptvencore.co.uk/product/the-importance-of-being-earnest-6290333578001
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -129,15 +146,35 @@ class TPTV(Service):
|
|||||||
tokens = cache.data
|
tokens = cache.data
|
||||||
else:
|
else:
|
||||||
self.log.info(" + Logging in...")
|
self.log.info(" + Logging in...")
|
||||||
payload = {"email": credential.username, "password": credential.password}
|
random_uuid = str(uuid.uuid4())
|
||||||
|
|
||||||
r = self.session.post(
|
r = self.session.post(
|
||||||
self.config["endpoints"]["login"],
|
self.config["endpoints"]["login"],
|
||||||
headers=self.session.headers,
|
headers=self.session.headers,
|
||||||
json={
|
|
||||||
"email": credential.username,
|
json= {
|
||||||
"password": credential.password,
|
"deviceInfo": {
|
||||||
|
"id": random_uuid,
|
||||||
|
"hardware": {
|
||||||
|
"manufacturer": "UNKNOWN/UNKNOWN",
|
||||||
|
"model": "Zimbadoo",
|
||||||
|
"version": "151.0"
|
||||||
|
},
|
||||||
|
"os": {
|
||||||
|
"name": "RisingHillOS",
|
||||||
|
"version": "x86_64"
|
||||||
|
},
|
||||||
|
"display": {
|
||||||
|
"width": 3840,
|
||||||
|
"height": 2048,
|
||||||
|
"formFactor": "THEATRE"
|
||||||
|
},
|
||||||
|
"legal": {}
|
||||||
},
|
},
|
||||||
|
"values": {
|
||||||
|
"email": credential.username,
|
||||||
|
"password": credential.password
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
res = r.json()
|
res = r.json()
|
||||||
@@ -152,74 +189,13 @@ class TPTV(Service):
|
|||||||
self.authorization = tokens
|
self.authorization = tokens
|
||||||
|
|
||||||
def search(self) -> Generator[SearchResult, None, None]:
|
def search(self) -> Generator[SearchResult, None, None]:
|
||||||
query = self.title.replace(" ", "+")
|
print(f"Searching not implemented in Envied for TPTVencore. Please use VineFeeder instead.")
|
||||||
search_url = self.config["endpoints"]["search"].replace("{query}", query)
|
return None
|
||||||
session_id = self.session.headers['session']
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0',
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Accept-Language': 'en-GB,en;q=0.5',
|
|
||||||
'Referer': 'https://tptvencore.co.uk/',
|
|
||||||
'session': session_id,
|
|
||||||
'tenant': 'encore',
|
|
||||||
'Origin': 'https://tptvencore.co.uk',
|
|
||||||
'DNT': '1',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
'Sec-Fetch-Dest': 'empty',
|
|
||||||
'Sec-Fetch-Mode': 'cors',
|
|
||||||
'Sec-Fetch-Site': 'cross-site',
|
|
||||||
'Priority': 'u=4',
|
|
||||||
}
|
|
||||||
r = self.session.get(search_url, headers=headers)
|
|
||||||
if r.status_code != 200:
|
|
||||||
raise ConnectionError(f"Search failed with {r.status_code}: {r.text}")
|
|
||||||
|
|
||||||
results = r.json()["data"]
|
|
||||||
myitems = []
|
|
||||||
if isinstance(results, list):
|
|
||||||
for result in results:
|
|
||||||
# do lookup on list item product or collection
|
|
||||||
if result.startswith('collection_'):
|
|
||||||
id = result.replace('collection_','')
|
|
||||||
collection_url = f"https://prod.suggestedtv.com/api/client/v1/collection/by-reference/{id}?extend=label"
|
|
||||||
response = self.session.get(collection_url, headers=headers)
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
for item in data['children']:
|
|
||||||
myitems.append(item['id'].replace('product_',''))
|
|
||||||
else:
|
|
||||||
print(f"Error: {response.status_code} - {response.text}")
|
|
||||||
else:
|
|
||||||
myitems.append(result.replace('product_',''))
|
|
||||||
mystring = ",".join(myitems)
|
|
||||||
|
|
||||||
continued_search_url = "https://prod.suggestedtv.com/api/client/v1/product?ids=" + mystring + "&extend=label"
|
'''def get_titles(self) -> Union[Movies, Series]:
|
||||||
|
|
||||||
response = self.session.get(continued_search_url, headers=headers)
|
|
||||||
response.raise_for_status
|
|
||||||
results = response.json()
|
|
||||||
if isinstance(results, list):
|
|
||||||
items = results
|
|
||||||
elif isinstance(results, dict) and "data" in results:
|
|
||||||
items = results["data"]
|
|
||||||
else:
|
|
||||||
raise ValueError("Unexpected search result format")
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
try:
|
|
||||||
yield SearchResult(
|
|
||||||
id_=item.get("reference"),
|
|
||||||
title=item.get("name"),
|
|
||||||
description=item.get("description", "").replace('\n', ' '),
|
|
||||||
label=item.get("type", ""),
|
|
||||||
url=f"https://tptvencore.co.uk/product/{item.get('reference')}",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to yield item: {e}")
|
|
||||||
|
|
||||||
def get_titles(self) -> Union[Movies, Series]:
|
|
||||||
data = self.get_data(self.title)
|
data = self.get_data(self.title)
|
||||||
ids = ",".join(data)
|
#ids = ",".join(data)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0',
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0',
|
||||||
@@ -237,30 +213,220 @@ class TPTV(Service):
|
|||||||
}
|
}
|
||||||
session_id = self.session.headers['session']
|
session_id = self.session.headers['session']
|
||||||
headers['session'] = session_id
|
headers['session'] = session_id
|
||||||
params = {
|
se_pattern = re.compile(
|
||||||
'ids': ids,
|
r"\(\s*S\s*(?P<series>\d+)\s*,\s*EP\s*(?P<episode>\d+)\s*\)",
|
||||||
'extend': 'label',
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
if len(data) > 1:
|
||||||
|
# multiple items, treat as a series
|
||||||
|
episodes = []
|
||||||
|
for item_id in data:
|
||||||
|
print(len(str(item_id)))
|
||||||
|
if len(str(item_id)) > 20: # Skip item IDs that too long
|
||||||
|
continue
|
||||||
|
# Skip this item if it too long to be valid.
|
||||||
|
|
||||||
|
try: # two possible endpoints for item data, try both
|
||||||
|
response = self.session.get(f'https://tptvencore.co.uk/api/core/catalog/item/{item_id}?locale=en', headers=headers)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise Exception()
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
response = self.session.get(f'https://tptvencore.co.uk/api/core/catalog/collection/{item_id}?locale=en', headers=headers)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise Exception()
|
||||||
|
except:
|
||||||
|
continue # next item if both endpoints fail, thus ignore.
|
||||||
|
|
||||||
|
|
||||||
|
mydata = json.loads(response.text)
|
||||||
|
|
||||||
|
f = open("tptv_debug.json", "w")
|
||||||
|
f.write(json.dumps(mydata, indent=4))
|
||||||
|
f.close()
|
||||||
|
console.print_json(data=mydata)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
item = mydata['data']
|
||||||
|
title = mydata.get("title")
|
||||||
|
# some series exist with season/episode info,(most do not),
|
||||||
|
# so we try to extract it from the title if possible
|
||||||
|
match = se_pattern.search(title)
|
||||||
|
if match:
|
||||||
|
season = int(match.group("series"))
|
||||||
|
number = int(match.group("episode"))
|
||||||
|
else:
|
||||||
|
season = 0
|
||||||
|
number = 0
|
||||||
|
|
||||||
|
episodes.append(
|
||||||
|
Episode(
|
||||||
|
id_=item["id"],
|
||||||
|
service=self.__class__,
|
||||||
|
title=title,
|
||||||
|
season=season or 0,
|
||||||
|
number=number or 0,
|
||||||
|
name="",
|
||||||
|
language="en",
|
||||||
|
data=item,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
#return Series(episodes)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
response = self.session.get(f'https://tptvencore.co.uk/api/core/catalog/collection/{data[0]}?page=1&pageSize=20&locale=en', headers=headers)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise ConnectionRefusedError()
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
response = self.session.get(f'https://tptvencore.co.uk/api/core/catalog/item/{data[0]}?locale=en', headers=headers)
|
||||||
|
except:
|
||||||
|
raise ConnectionError("Failed to retrieve title data")
|
||||||
|
|
||||||
|
mydata = json.loads(response.text)
|
||||||
|
titles = mydata['data']
|
||||||
|
|
||||||
|
episodes = []
|
||||||
|
season = 0
|
||||||
|
number = 0
|
||||||
|
if (len(mydata)) > 1:
|
||||||
|
for item in titles:
|
||||||
|
title = item["title"]
|
||||||
|
# some series exist with season/episode info,(most do not),
|
||||||
|
# so we try to extract it from the title if possible
|
||||||
|
match = se_pattern.search(title)
|
||||||
|
if match:
|
||||||
|
season = int(match.group("series"))
|
||||||
|
number = int(match.group("episode"))
|
||||||
|
|
||||||
|
episodes.append(
|
||||||
|
Episode(
|
||||||
|
id_=item["id"],
|
||||||
|
service=self.__class__,
|
||||||
|
title=title,
|
||||||
|
season=season or 0,
|
||||||
|
number=number or 0,
|
||||||
|
|
||||||
|
name="",
|
||||||
|
language="en",
|
||||||
|
data=item,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# single item, treat as a movie
|
||||||
|
item = titles
|
||||||
|
title = item["title"]
|
||||||
|
# some series exist with season/episode info,(most do not),
|
||||||
|
# so we try to extract it from the title if possible
|
||||||
|
match = se_pattern.search(title)
|
||||||
|
if match:
|
||||||
|
season = int(match.group("series"))
|
||||||
|
number = int(match.group("episode"))
|
||||||
|
episodes.append(
|
||||||
|
Episode(
|
||||||
|
id_=item["id"],
|
||||||
|
service=self.__class__,
|
||||||
|
title=item["title"],
|
||||||
|
season=season or 0,
|
||||||
|
number=number or 0,
|
||||||
|
name="",
|
||||||
|
language="en",
|
||||||
|
data=item,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return Series(episodes)'''
|
||||||
|
|
||||||
|
def get_titles(self) -> Union[Movies, Series]:
|
||||||
|
data = self.get_data(self.title)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0",
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Accept-Language": "en-GB,en;q=0.5",
|
||||||
|
"Referer": "https://tptvencore.co.uk/",
|
||||||
|
"tenant": "encore",
|
||||||
|
"Origin": "https://tptvencore.co.uk",
|
||||||
|
"DNT": "1",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Sec-Fetch-Dest": "empty",
|
||||||
|
"Sec-Fetch-Mode": "cors",
|
||||||
|
"Sec-Fetch-Site": "cross-site",
|
||||||
|
"Priority": "u=4",
|
||||||
|
"session": self.session.headers["session"],
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.session.get('https://prod.suggestedtv.com/api/client/v1/product', params=params, headers=headers)
|
se_pattern = re.compile(
|
||||||
if response.status_code == 200:
|
r"\(\s*S\s*(?P<season>\d+)\s*,\s*EP\s*(?P<episode>\d+)\s*\)",
|
||||||
mydata=json.loads(response.text)
|
re.IGNORECASE,
|
||||||
titles = mydata['data']
|
)
|
||||||
episodes =[
|
|
||||||
Episode(
|
|
||||||
id_=episode["id"],
|
|
||||||
service=self.__class__,
|
|
||||||
title=episode["name"],
|
|
||||||
season=0,
|
|
||||||
number=0,
|
|
||||||
name = '',
|
|
||||||
language="en", # TODO: language detection
|
|
||||||
data=episode,
|
|
||||||
)
|
|
||||||
for episode in titles
|
|
||||||
]
|
|
||||||
return Series(episodes)
|
|
||||||
|
|
||||||
|
def extract_se(title: str) -> tuple[int, int]:
|
||||||
|
match = se_pattern.search(title or "")
|
||||||
|
if not match:
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
return int(match.group("season")), int(match.group("episode"))
|
||||||
|
|
||||||
|
def fetch_json(item_id: str | int) -> dict | None:
|
||||||
|
"""
|
||||||
|
Try item endpoint first, then collection endpoint.
|
||||||
|
Return decoded JSON or None if both fail.
|
||||||
|
"""
|
||||||
|
urls = [
|
||||||
|
f"https://tptvencore.co.uk/api/core/catalog/item/{item_id}?locale=en",
|
||||||
|
f"https://tptvencore.co.uk/api/core/catalog/collection/{item_id}?page=1&pageSize=20&locale=en",
|
||||||
|
f"https://tptvencore.co.uk/api/core/catalog/collection/{item_id}?locale=en",
|
||||||
|
]
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
response = self.session.get(url, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def make_episode(item: dict) -> Episode:
|
||||||
|
title = item.get("title", "")
|
||||||
|
season, number = extract_se(title)
|
||||||
|
|
||||||
|
return Episode(
|
||||||
|
id_=item["id"],
|
||||||
|
service=self.__class__,
|
||||||
|
title=title,
|
||||||
|
season=season,
|
||||||
|
number=number,
|
||||||
|
name="",
|
||||||
|
language="en",
|
||||||
|
data=item,
|
||||||
|
)
|
||||||
|
|
||||||
|
episodes = []
|
||||||
|
|
||||||
|
for item_id in data:
|
||||||
|
# Skip bogus IDs
|
||||||
|
if len(str(item_id)) > 20:
|
||||||
|
continue
|
||||||
|
|
||||||
|
mydata = fetch_json(item_id)
|
||||||
|
if not mydata:
|
||||||
|
continue
|
||||||
|
|
||||||
|
titles = mydata.get("data")
|
||||||
|
|
||||||
|
if not titles:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Collection endpoint may return a list of items
|
||||||
|
if isinstance(titles, list):
|
||||||
|
for item in titles:
|
||||||
|
episodes.append(make_episode(item))
|
||||||
|
|
||||||
|
# Item endpoint returns a single dict
|
||||||
|
elif isinstance(titles, dict):
|
||||||
|
episodes.append(make_episode(titles))
|
||||||
|
|
||||||
|
return Series(episodes)
|
||||||
|
|
||||||
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
|
def get_tracks(self, title: Union[Movie, Episode]) -> Tracks:
|
||||||
playlist = f"https://edge.api.brightcove.com/playback/v1/accounts/6272132012001/videos/{title.data.get('id')}"
|
playlist = f"https://edge.api.brightcove.com/playback/v1/accounts/6272132012001/videos/{title.data.get('id')}"
|
||||||
@@ -314,24 +480,25 @@ class TPTV(Service):
|
|||||||
|
|
||||||
def get_data(self, url: str) -> dict:
|
def get_data(self, url: str) -> dict:
|
||||||
self.session.headers.update({'tenant': 'encore'})
|
self.session.headers.update({'tenant': 'encore'})
|
||||||
if 'details' in url:
|
if 'COLLECTION' in url:
|
||||||
prod_id = url.split('item/')[-2]
|
prod_id = url.split('/')[-2]
|
||||||
url = f"https://prod.suggestedtv.com/api/client/v1/collection/by-reference/{prod_id}?extend=label"
|
url = f"https://tptvencore.co.uk/api/core/catalog/collection/{prod_id}"
|
||||||
|
|
||||||
r = self.session.get(url)
|
r = self.session.get(url)
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
raise ConnectionError(r.text)
|
raise ConnectionError(r.text)
|
||||||
|
|
||||||
myjson = r.json()
|
myjson = r.json()
|
||||||
children = myjson.get('children')
|
data = myjson.get("data", {})
|
||||||
product_links = []
|
product_links = []
|
||||||
for child in children:
|
for child in data:
|
||||||
product_links.append(child['id']) if 'product' in child.get('classification') else None
|
product_links.append(child.get('id', {})) if child.get('id', {}) else None
|
||||||
return product_links
|
return product_links
|
||||||
elif 'playback' in url: # single item
|
elif 'playback' in url: # single item
|
||||||
prod_id = url.split('/')[-1]
|
prod_id = url.split('/')[-1]
|
||||||
return [prod_id]
|
return [prod_id]
|
||||||
elif 'VIDEO' in url:
|
elif 'details' in url:
|
||||||
prod_id = url.split('/')[-2]
|
prod_id = url.split('/')[-2]
|
||||||
return [prod_id]
|
return [prod_id]
|
||||||
else:
|
else:
|
||||||
raise ValueError("URL format not recognized for data retrieval.")
|
raise ValueError("URL format not recognized for data retrieval in proc get_data().")
|
||||||
@@ -17,7 +17,7 @@ session:
|
|||||||
api-key: zq5pyPd0RTbNg3Fyj52PrkKL9c2Af38HHh4itgZTKDaCzjAyhd
|
api-key: zq5pyPd0RTbNg3Fyj52PrkKL9c2Af38HHh4itgZTKDaCzjAyhd
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
login: https://prod.suggestedtv.com/api/client/v1/session/login
|
login: https://tptvencore.co.uk/api/core/auth/login?locale=en
|
||||||
session: https://prod.suggestedtv.com/api/client/v1/session
|
session: https://prod.suggestedtv.com/api/client/v1/session
|
||||||
search: https://prod.suggestedtv.com/api/client/v2/search/{query}
|
search: https://tptvencore.co.uk/api/core/search?q={query}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class BaseLoader:
|
|||||||
|
|
||||||
def fetch_and_parse(self, url: str, headers):
|
def fetch_and_parse(self, url: str, headers):
|
||||||
# deals with response.text as xml
|
# deals with response.text as xml
|
||||||
# first apporach - request json
|
# first approach - request json
|
||||||
# second approach convert xml to json
|
# second approach convert xml to json
|
||||||
with httpx.Client(timeout=30) as client:
|
with httpx.Client(timeout=30) as client:
|
||||||
resp = client.get(url, headers=headers)
|
resp = client.get(url, headers=headers)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
BATCH_DOWNLOAD: false
|
BATCH_DOWNLOAD: false
|
||||||
TERMINAL: gnome-terminal
|
TERMINAL: gnome-terminal
|
||||||
TERMINAL_RESET: true
|
TERMINAL_RESET: false
|
||||||
|
|||||||
@@ -82,13 +82,13 @@ class TptvLoader(BaseLoader):
|
|||||||
|
|
||||||
if opts:
|
if opts:
|
||||||
TptvLoader.options = opts
|
TptvLoader.options = opts
|
||||||
self.options_list = split_options(TptvLoader.options)
|
self.options_list = split_options(TptvLoader.options)
|
||||||
# direct download
|
# direct download
|
||||||
|
|
||||||
if "http" in search_term and inx == 1:
|
if "http" in search_term and not 'COLLECTION' in search_term and inx == 1:
|
||||||
try:
|
try:
|
||||||
self.options_list = split_options(TptvLoader.options)
|
#self.options_list = split_options(TptvLoader.options)
|
||||||
if self.options_list[0] == "":
|
if not opts:
|
||||||
command = ['uv', 'run', 'envied', "dl", "TPTV", search_term]
|
command = ['uv', 'run', 'envied', "dl", "TPTV", search_term]
|
||||||
else:
|
else:
|
||||||
command = ['uv', 'run', 'envied', "dl", *self.options_list, "TPTV", search_term]
|
command = ['uv', 'run', 'envied', "dl", *self.options_list, "TPTV", search_term]
|
||||||
@@ -101,7 +101,10 @@ class TptvLoader(BaseLoader):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
elif "COLLECTION" in search_term and inx == 1:
|
||||||
|
search_term = search_term.split('/')[-1]
|
||||||
|
inx = 3
|
||||||
|
return self.fetch_videos(search_term)
|
||||||
# keyword search
|
# keyword search
|
||||||
elif inx == 3:
|
elif inx == 3:
|
||||||
print(f"Searching for {search_term}")
|
print(f"Searching for {search_term}")
|
||||||
@@ -128,13 +131,13 @@ class TptvLoader(BaseLoader):
|
|||||||
) # search_term here holds a URL!!!
|
) # search_term here holds a URL!!!
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(f"Unknown error when searching for {search_term}")
|
print(f"Either an incorrect url was entered or an unknown error when searching for {search_term}")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def fetch_videos(self, search_term):
|
def fetch_videos(self, search_term):
|
||||||
|
|
||||||
suggested_url = f"https://prod.suggestedtv.com/api/client/v2/search/{search_term}"
|
suggested_url = f"https://tptvencore.co.uk/api/core/search?q={search_term}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -152,38 +155,21 @@ class TptvLoader(BaseLoader):
|
|||||||
get_headers['Access-Control-Request-Headers'] = 'session,tenant'
|
get_headers['Access-Control-Request-Headers'] = 'session,tenant'
|
||||||
get_headers['Access-Control-Request-Method'] = 'GET'
|
get_headers['Access-Control-Request-Method'] = 'GET'
|
||||||
get_headers['session'] = self.session_id
|
get_headers['session'] = self.session_id
|
||||||
get_headers['tenant'] = 'encore'
|
#get_headers['tenant'] = 'encore'
|
||||||
response = self.get_data(suggested_url, headers=get_headers)
|
response = self.get_data(suggested_url, headers=get_headers)
|
||||||
myjson = json.loads(response)
|
myjson = json.loads(response)
|
||||||
|
|
||||||
myitems = []
|
|
||||||
for item in myjson['data']:
|
|
||||||
if item.startswith('collection_'):
|
|
||||||
id = item.replace('collection_','')
|
|
||||||
collection_url = f"https://prod.suggestedtv.com/api/client/v1/collection/by-reference/{id}?extend=label"
|
|
||||||
response = self.get_data(collection_url, headers=get_headers)
|
|
||||||
if response:
|
|
||||||
data = json.loads(response)
|
|
||||||
for item in data['children']:
|
|
||||||
myitems.append(item['id'].replace('product_',''))
|
|
||||||
elif item.startswith('product_'):
|
|
||||||
myitems.append(item.replace('product_',''))
|
|
||||||
mystring = ",".join(myitems)
|
|
||||||
|
|
||||||
continued_search_url = "https://prod.suggestedtv.com/api/client/v1/product?ids=" + mystring + "&extend=label"
|
|
||||||
|
|
||||||
response = self.get_data(continued_search_url, headers=get_headers)
|
|
||||||
data = json.loads(response)
|
|
||||||
try:
|
try:
|
||||||
for item in data['data']:
|
for item in myjson['data']:
|
||||||
vid_id = item['reference']
|
url = item['video']['playback'].replace("api/core/play", "playback")
|
||||||
title = item['name']
|
title = item['title']
|
||||||
synopsis = item['description'].replace('\n', ' ')
|
synopsis = item['description'].replace('\n', ' ')
|
||||||
if len(synopsis) > 300:
|
if len(synopsis) > 300:
|
||||||
synopsis = f"{synopsis[:300]} ...snip"
|
synopsis = f"{synopsis[:300]} ...snip"
|
||||||
episode = {
|
episode = {
|
||||||
"title": title,
|
"title": title,
|
||||||
"url": vid_id,
|
"url": url,
|
||||||
"synopsis": synopsis,
|
"synopsis": synopsis,
|
||||||
}
|
}
|
||||||
self.add_episode(title,episode)
|
self.add_episode(title,episode)
|
||||||
@@ -211,7 +197,8 @@ class TptvLoader(BaseLoader):
|
|||||||
self.options_list = split_options(self.options)
|
self.options_list = split_options(self.options)
|
||||||
for item in selected:
|
for item in selected:
|
||||||
url = item[2]
|
url = item[2]
|
||||||
url = f"https://tptvencore.co.uk/product/{url}"
|
id = url.split("/")[-1].replace("?locale=en", "")
|
||||||
|
url = f'https://tptvencore.co.uk/playback/item/{id}'
|
||||||
#print(url)
|
#print(url)
|
||||||
try:
|
try:
|
||||||
if self.options_list[0] == "":
|
if self.options_list[0] == "":
|
||||||
|
|||||||
Reference in New Issue
Block a user