forked from TPD94/devine
Added VIKI service
- Supports both Series & Movies - Supports both Free & Paid Titles - Adds Language Tag - Without cookies, gets SD for Free Titlesmaster^2
parent
343d973e82
commit
e284a723e1
|
@ -0,0 +1,167 @@
|
|||
import base64
|
||||
import re
|
||||
import math
|
||||
import json
|
||||
import time
|
||||
import datetime
|
||||
import logging
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from http.cookiejar import CookieJar
|
||||
from typing import Optional, Union
|
||||
from urllib.parse import urlparse
|
||||
import click
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
from rich.padding import Padding
|
||||
from rich.rule import Rule
|
||||
from devine.core.service import Service
|
||||
from devine.core.titles import Series, Movies, Movie, Episode
|
||||
from devine.core.cacher import Cacher
|
||||
from devine.core.config import config
|
||||
from devine.core.console import console
|
||||
from devine.core.constants import AnyTrack
|
||||
from devine.core.credential import Credential
|
||||
from devine.core.titles import Title_T, Titles_T
|
||||
from devine.core.tracks import Video, Audio, Subtitle, Chapter, Chapters, Tracks
|
||||
from devine.core.utilities import get_ip_info
|
||||
from devine.core.manifests import HLS, DASH
|
||||
|
||||
class VIKI(Service):
|
||||
"""
|
||||
Service code for Viki
|
||||
Written by ToonsHub
|
||||
|
||||
Authorization: None (Free SD) | Cookies (Free and Paid Titles)
|
||||
Security: FHD@L3
|
||||
"""
|
||||
|
||||
# Static method, this method belongs to the class
|
||||
@staticmethod
|
||||
|
||||
# The command name, must much the service tag (and by extension the service folder)
|
||||
@click.command(name="VIKI", short_help="https://www.viki.com", help=__doc__)
|
||||
|
||||
# Using series ID
|
||||
@click.argument("title", type=str)
|
||||
|
||||
# Movie Tag
|
||||
@click.option("-m", "--movie", is_flag=True, default=False, help="Title is a Movie.")
|
||||
|
||||
# Pass the context back to the CLI with arguments
|
||||
@click.pass_context
|
||||
def cli(ctx, **kwargs):
|
||||
return VIKI(ctx, **kwargs)
|
||||
|
||||
# Accept the CLI arguments by overriding the constructor (The __init__() method)
|
||||
def __init__(self, ctx, title, movie):
|
||||
|
||||
# Pass the series_id argument to self so it's accessable across all methods
|
||||
self.title = title
|
||||
self.is_movie = movie
|
||||
|
||||
# Overriding the constructor
|
||||
super().__init__(ctx)
|
||||
|
||||
|
||||
# Defining an authinticate function
|
||||
def authenticate(self, cookies: Optional[CookieJar], credential: Optional[Credential] = None):
|
||||
|
||||
# Save Cookies
|
||||
if cookies:
|
||||
self.session.cookies.get(cookies)
|
||||
self.session.cookies.update(cookies)
|
||||
|
||||
# Defining a function to return titles
|
||||
def get_titles(self):
|
||||
|
||||
if not self.is_movie:
|
||||
|
||||
# Get the metadata needed for the series
|
||||
series_metadata = self.session.get(f'https://api.viki.io/v4/containers/{self.title}/episodes.json?direction=asc&with_upcoming=false&sort=number&page=1&per_page=100&app=100000a')
|
||||
|
||||
# Set an empty list for episodes
|
||||
episodes = []
|
||||
|
||||
# Get the episode metadata by iterating through each season id
|
||||
for episode in series_metadata.json()['response']:
|
||||
|
||||
# Get the id
|
||||
episode_id = episode["id"]
|
||||
|
||||
# Get the show title
|
||||
show_title = episode["container"]["titles"]["en"]
|
||||
|
||||
# Get the season
|
||||
episode_season = 1
|
||||
|
||||
# Get the episode number
|
||||
episode_number = episode["number"]
|
||||
|
||||
# Get the episode name
|
||||
episode_name = None
|
||||
|
||||
# Get the episode year
|
||||
episode_year = episode["created_at"][:4]
|
||||
|
||||
# Set a class for each episode
|
||||
episode_class = Episode(id_=episode_id, title=show_title, season=episode_season, number=episode_number, name=episode_name, year=episode_year, service=self.__class__)
|
||||
|
||||
# Append it to the list
|
||||
episodes.append(episode_class)
|
||||
|
||||
# Return the episodes as a Series object
|
||||
return Series(episodes)
|
||||
|
||||
else:
|
||||
|
||||
# Get Video API URL
|
||||
page_html = requests.get(f"https://www.viki.com/movies/{self.title}").text
|
||||
video_id = re.search(r'https://api.viki.io/v4/videos/(.*?).json', page_html).group(1)
|
||||
|
||||
# Get Movie Data
|
||||
movie_metadata = self.session.get(f'https://api.viki.io/v4/videos/{video_id}.json?app=100000a').json()
|
||||
movie_id = movie_metadata["id"]
|
||||
movie_name = movie_metadata["titles"]["en"]
|
||||
movie_year = movie_metadata["created_at"][:4]
|
||||
movie_class = Movie(id_=movie_id, name=movie_name, year=movie_year, service=self.__class__)
|
||||
|
||||
return Movies([movie_class])
|
||||
|
||||
|
||||
# Defining a function to get tracks
|
||||
def get_tracks(self, title: Title_T) -> Tracks:
|
||||
|
||||
# Set the headers for the request
|
||||
headers = {
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 OPR/107.0.0.0',
|
||||
'x-client-user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 OPR/107.0.0.0',
|
||||
'x-viki-app-ver': '14.10.0',
|
||||
'x-viki-as-id': '100000a-1709757058757-0fb4be98-a04e-47b2-a80b-2dfe75cc6376',
|
||||
}
|
||||
|
||||
# Update the headers
|
||||
self.session.headers.update(headers)
|
||||
|
||||
mpd_info = self.session.get(f'https://www.viki.com/api/videos/{title.id}')
|
||||
mpd_url = mpd_info.json()["queue"][1]["url"]
|
||||
mpd_lang = mpd_info.json()["video"]["origin"]["language"]
|
||||
self.license_url = json.loads(base64.b64decode(mpd_info.json()["drm"]).decode("utf-8", "ignore"))["dt3"]
|
||||
|
||||
# Grab the tracks from the MPD
|
||||
tracks = DASH.from_url(url=mpd_url).to_tracks(language=mpd_lang)
|
||||
|
||||
# Return the tracks
|
||||
return tracks
|
||||
|
||||
# Defining a function to get chapters
|
||||
def get_chapters(self, title):
|
||||
return []
|
||||
|
||||
# Defining a function to get widevine license keys
|
||||
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
|
||||
|
||||
# Send the post request to the license server
|
||||
license_raw = self.session.post(self.license_url, data=challenge).content
|
||||
|
||||
# Return the license
|
||||
return base64.b64encode(license_raw).decode()
|
Loading…
Reference in New Issue