547 lines
22 KiB
C#
547 lines
22 KiB
C#
using Microsoft.AspNetCore.Http;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Newtonsoft.Json;
|
||
using Newtonsoft.Json.Linq;
|
||
using Shared.Models.Online.Lumex;
|
||
using Shared.Models.Online.Settings;
|
||
using System.Text;
|
||
|
||
namespace Online.Controllers
|
||
{
|
||
public class VideoCDN : BaseOnlineController<LumexSettings>
|
||
{
|
||
static VideoCDN()
|
||
{
|
||
Directory.CreateDirectory("cache/logs/VideoCDN");
|
||
}
|
||
|
||
public VideoCDN() : base(AppInit.conf.VideoCDN)
|
||
{
|
||
loadKitInitialization = (j, i, c) =>
|
||
{
|
||
if (j.ContainsKey("log"))
|
||
i.log = c.log;
|
||
|
||
i.clientId = c.clientId;
|
||
i.username = c.username;
|
||
i.password = c.password;
|
||
i.domain = Regex.Replace(c.domain ?? "bwa", "^https?://", "").Split(".")[0];
|
||
i.corseu = false;
|
||
|
||
return i;
|
||
};
|
||
|
||
requestInitialization = () =>
|
||
{
|
||
init.rhub = !init.disable_protection;
|
||
};
|
||
}
|
||
|
||
[HttpGet]
|
||
[Route("lite/videocdn")]
|
||
async public Task<ActionResult> Index(long content_id, string content_type, string imdb_id, long kinopoisk_id, string title, string original_title, string t, int clarification, bool similar = false, int s = -1, int serial = -1, bool rjson = false, bool checksearch = false)
|
||
{
|
||
if (await IsRequestBlocked(rch: true))
|
||
return badInitMsg;
|
||
|
||
if (string.IsNullOrEmpty(init.username) || string.IsNullOrEmpty(init.password))
|
||
return OnError();
|
||
|
||
if (content_id == 0)
|
||
{
|
||
var search = await InvokeCache($"videocdn:search:{imdb_id}:{kinopoisk_id}:{title ?? original_title}:{clarification}:{similar}", 60,
|
||
() => Search(imdb_id, kinopoisk_id, title, original_title, serial, clarification, similar)
|
||
);
|
||
|
||
if (search.content_type == null && search.similar?.data == null)
|
||
return OnError();
|
||
|
||
if (search.similar?.data != null)
|
||
return await ContentTpl(search.similar);
|
||
|
||
content_id = search.content_id;
|
||
content_type = search.content_type;
|
||
}
|
||
|
||
if (content_id == 0 || string.IsNullOrEmpty(content_type))
|
||
return OnError();
|
||
|
||
if (checksearch)
|
||
return Content("data-json=");
|
||
|
||
string accessToken = await getToken();
|
||
if (string.IsNullOrEmpty(accessToken))
|
||
return OnError();
|
||
|
||
var player = await getPlayer(content_id, content_type, accessToken);
|
||
if (player == null)
|
||
return OnError();
|
||
|
||
if (player.content_type is "movie" or "anime")
|
||
{
|
||
#region Фильм
|
||
var mtpl = new MovieTpl(title, original_title, player.media.Length);
|
||
|
||
foreach (var media in player.media)
|
||
{
|
||
string hash = CrypTo.md5($"{init.clientId}:{content_type}:{content_id}:{media.playlist}:{requestInfo.IP}");
|
||
string link = accsArgs($"{host}/lite/videocdn/video?rjson={rjson}&content_id={content_id}&content_type={content_type}&playlist={HttpUtility.UrlEncode(media.playlist)}&max_quality={media.max_quality}&translation_id={media.translation_id}&hash={hash}");
|
||
string streamlink = link.Replace("/videocdn/video", "/videocdn/video.m3u8") + "&play=true";
|
||
|
||
mtpl.Append(media.translation_name, link, "call", streamlink, quality: media.max_quality?.ToString());
|
||
}
|
||
|
||
return await ContentTpl(mtpl);
|
||
#endregion
|
||
}
|
||
else
|
||
{
|
||
#region Сериал
|
||
string enc_title = HttpUtility.UrlEncode(title);
|
||
string enc_original_title = HttpUtility.UrlEncode(original_title);
|
||
|
||
if (s == -1)
|
||
{
|
||
var tpl = new SeasonTpl(player.media.First().max_quality?.ToString(), player.media.Length);
|
||
|
||
foreach (var media in player.media.OrderBy(s => s.season_id))
|
||
{
|
||
string link = $"{host}/lite/videocdn?rjson={rjson}&content_id={content_id}&content_type={content_type}&title={enc_title}&original_title={enc_original_title}&s={media.season_id}";
|
||
tpl.Append($"{media.season_id} сезон", link, media.season_id);
|
||
}
|
||
|
||
return await ContentTpl(tpl);
|
||
}
|
||
else
|
||
{
|
||
#region Перевод
|
||
var vtpl = new VoiceTpl();
|
||
var tmpVoice = new HashSet<int>();
|
||
|
||
foreach (var media in player.media)
|
||
{
|
||
if (media.season_id != s)
|
||
continue;
|
||
|
||
foreach (var episode in media.episodes)
|
||
{
|
||
foreach (var voice in episode.media)
|
||
{
|
||
if (tmpVoice.Contains(voice.translation_id))
|
||
continue;
|
||
|
||
tmpVoice.Add(voice.translation_id);
|
||
|
||
if (string.IsNullOrEmpty(t))
|
||
t = voice.translation_id.ToString();
|
||
|
||
vtpl.Append(voice.translation_name, t == voice.translation_id.ToString(), $"{host}/lite/videocdn?rjson={rjson}&content_id={content_id}&content_type={content_type}&title={enc_title}&original_title={enc_original_title}&s={s}&t={voice.translation_id}");
|
||
}
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
if (string.IsNullOrEmpty(t))
|
||
t = "0";
|
||
|
||
var etpl = new EpisodeTpl(vtpl);
|
||
|
||
foreach (var media in player.media)
|
||
{
|
||
if (media.season_id != s)
|
||
continue;
|
||
|
||
foreach (var episode in media.episodes)
|
||
{
|
||
foreach (var voice in episode.media)
|
||
{
|
||
if (voice.translation_id.ToString() != t)
|
||
continue;
|
||
|
||
string hash = CrypTo.md5($"{init.clientId}:{content_type}:{content_id}:{voice.playlist}:{requestInfo.IP}");
|
||
string link = accsArgs($"{host}/lite/videocdn/video?content_id={content_id}&content_type={content_type}&playlist={HttpUtility.UrlEncode(voice.playlist)}&max_quality={voice.max_quality}&s={s}&e={episode.episode_id}&translation_id={voice.translation_id}&hash={hash}&serial=true");
|
||
string streamlink = link.Replace("/videocdn/video", "/videocdn/video.m3u8") + "&play=true";
|
||
|
||
etpl.Append($"{episode.episode_id} серия", title ?? original_title, s.ToString(), episode.episode_id.ToString(), link, "call", streamlink: streamlink);
|
||
}
|
||
}
|
||
}
|
||
|
||
return await ContentTpl(etpl);
|
||
}
|
||
#endregion
|
||
}
|
||
}
|
||
|
||
|
||
#region Video
|
||
static FileStream logFileStream = null;
|
||
|
||
[HttpGet]
|
||
[Route("lite/videocdn/video")]
|
||
[Route("lite/videocdn/video.m3u8")]
|
||
async public ValueTask<ActionResult> Video(string hash, long content_id, string content_type, string playlist, int max_quality, bool play, bool serial, int s, int e, int translation_id)
|
||
{
|
||
if (await IsRequestBlocked(rch: true, rch_check: false))
|
||
return badInitMsg;
|
||
|
||
if (hash != CrypTo.md5($"{init.clientId}:{content_type}:{content_id}:{playlist}:{requestInfo.IP}"))
|
||
return OnError("hash", gbcache: false);
|
||
|
||
if (rch != null)
|
||
{
|
||
if (rch.IsNotConnected())
|
||
{
|
||
if (init.rhub_fallback && play)
|
||
rch.Disabled();
|
||
else
|
||
return ContentTo(rch.connectionMsg);
|
||
}
|
||
|
||
if (!play && rch.IsRequiredConnected())
|
||
return ContentTo(rch.connectionMsg);
|
||
}
|
||
|
||
string accessToken = await getToken();
|
||
if (string.IsNullOrEmpty(accessToken))
|
||
return OnError("token", gbcache: false);
|
||
|
||
try
|
||
{
|
||
if (init.log)
|
||
{
|
||
string data = System.Text.Json.JsonSerializer.Serialize(new
|
||
{
|
||
time = DateTime.Now,
|
||
requestInfo.Country,
|
||
requestInfo.IP,
|
||
requestInfo.UserAgent,
|
||
video = new { content_id, content_type, playlist, accessToken }
|
||
});
|
||
|
||
string patchlog = $"cache/logs/VideoCDN/{DateTime.Today:dd-MM}.txt";
|
||
|
||
if (logFileStream == null || !System.IO.File.Exists(patchlog))
|
||
logFileStream = new FileStream(patchlog, FileMode.Append, FileAccess.Write);
|
||
|
||
var buffer = Encoding.UTF8.GetBytes($"{data}\n");
|
||
await logFileStream.WriteAsync(buffer);
|
||
await logFileStream.FlushAsync();
|
||
}
|
||
}
|
||
catch { }
|
||
|
||
string clientIP = init.verifyip ? requestInfo.IP : "::1";
|
||
string memkey = $"videocdn/video:{playlist}:{(init.streamproxy ? "" : clientIP)}";
|
||
|
||
return await InvkSemaphore(memkey, async () =>
|
||
{
|
||
if (!hybridCache.TryGetValue(memkey, out string hls))
|
||
{
|
||
var headers = HeadersModel.Init("Authorization", $"Bearer {accessToken}");
|
||
|
||
if (!init.streamproxy)
|
||
headers.Add(new("X-LAMPA-CLIENT-IP", clientIP));
|
||
|
||
var result = await httpHydra.Post<JObject>(init.apihost + playlist, "{}", addheaders: headers);
|
||
|
||
if (result == null || !result.ContainsKey("url"))
|
||
return OnError(null, gbcache: false);
|
||
|
||
string url = result.Value<string>("url");
|
||
if (string.IsNullOrEmpty(url))
|
||
return OnError(null, gbcache: false);
|
||
|
||
if (url.StartsWith("/"))
|
||
hls = $"{init.scheme}:{url}";
|
||
else
|
||
hls = url;
|
||
|
||
hybridCache.Set(memkey, hls, DateTime.Now.AddMinutes(10));
|
||
}
|
||
|
||
if (play)
|
||
return Redirect(HostStreamProxy(hls));
|
||
|
||
var player = await getPlayer(content_id, content_type, accessToken);
|
||
VastConf vast = requestInfo.user != null ? null : new VastConf() { url = player?.tag_url, msg = init?.vast?.msg };
|
||
if (init.disable_ads)
|
||
vast = null;
|
||
|
||
#region subtitle
|
||
var subtitles = new SubtitleTpl();
|
||
|
||
try
|
||
{
|
||
if (translation_id > 0)
|
||
{
|
||
if (serial)
|
||
{
|
||
if (e > 0 && s > 0)
|
||
{
|
||
foreach (var media in player.media.Where(i => i.season_id == s))
|
||
{
|
||
foreach (var episode in media.episodes.Where(i => i.episode_id == e))
|
||
{
|
||
foreach (var voice in episode.media.Where(i => i.translation_id == translation_id))
|
||
{
|
||
if (voice.tracks != null)
|
||
{
|
||
foreach (var t in voice.tracks)
|
||
subtitles.Append(t.label ?? t.srlang, $"{init.scheme}:{t.src}");
|
||
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var tracks = player.media.FirstOrDefault(i => i.translation_id == translation_id).tracks;
|
||
if (tracks != null)
|
||
{
|
||
foreach (var t in tracks)
|
||
subtitles.Append(t.label ?? t.srlang, $"{init.scheme}:{t.src}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch { }
|
||
#endregion
|
||
|
||
if (max_quality > 0 && !init.hls)
|
||
{
|
||
var streamquality = new StreamQualityTpl();
|
||
foreach (int q in new int[] { 1080, 720, 480, 360, 240 })
|
||
{
|
||
if (max_quality >= q)
|
||
streamquality.Append(HostStreamProxy(Regex.Replace(hls, "/hls\\.m3u8$", $"/{q}.mp4")), $"{q}p");
|
||
}
|
||
|
||
if (!streamquality.Any())
|
||
return OnError("streams");
|
||
|
||
var first = streamquality.Firts();
|
||
return ContentTo(VideoTpl.ToJson("play", first.link, first.quality, streamquality: streamquality, subtitles: subtitles, vast: vast));
|
||
}
|
||
|
||
return ContentTo(VideoTpl.ToJson("play", HostStreamProxy(hls), "auto", subtitles: subtitles, vast: vast));
|
||
});
|
||
}
|
||
#endregion
|
||
|
||
|
||
#region getToken
|
||
async ValueTask<string> getToken()
|
||
{
|
||
#region refreshToken
|
||
string memKey = $"videocdn:refreshToken:{init.username}";
|
||
if (!hybridCache.TryGetValue(memKey, out string refreshToken))
|
||
{
|
||
var data = new System.Net.Http.StringContent($"{{\"username\":\"{init.username}\",\"password\":\"{init.password}\"}}", Encoding.UTF8, "application/json");
|
||
var job = await Http.Post<JObject>($"{init.apihost}/login", data, useDefaultHeaders: false, proxy: proxy);
|
||
if (job == null || !job.ContainsKey("refreshToken"))
|
||
return null;
|
||
|
||
refreshToken = job.Value<string>("refreshToken");
|
||
if (string.IsNullOrEmpty(refreshToken))
|
||
return null;
|
||
|
||
hybridCache.Set(memKey, refreshToken, DateTime.Now.AddDays(2));
|
||
}
|
||
#endregion
|
||
|
||
string clientIP = init.verifyip ? requestInfo.IP : "::1";
|
||
|
||
memKey = $"videocdn:accessToken:{(init.streamproxy ? "" : clientIP)}";
|
||
if (!hybridCache.TryGetValue(memKey, out string accessToken))
|
||
{
|
||
var headers = init.streamproxy ? null : HeadersModel.Init(("X-LAMPA-CLIENT-IP", clientIP));
|
||
|
||
var data = new System.Net.Http.StringContent($"{{\"token\":\"{refreshToken}\"}}", Encoding.UTF8, "application/json");
|
||
var job = await Http.Post<JObject>($"{init.apihost}/refresh", data, useDefaultHeaders: false, headers: headers, proxy: proxy);
|
||
if (job == null || !job.ContainsKey("accessToken"))
|
||
return null;
|
||
|
||
accessToken = job.Value<string>("accessToken");
|
||
if (string.IsNullOrEmpty(accessToken))
|
||
return null;
|
||
|
||
hybridCache.Set(memKey, accessToken, DateTime.Now.AddMinutes(5));
|
||
}
|
||
|
||
return accessToken;
|
||
}
|
||
#endregion
|
||
|
||
#region getPlayer
|
||
async ValueTask<EmbedModel> getPlayer(long content_id, string content_type, string accessToken)
|
||
{
|
||
if (content_id == 0 || string.IsNullOrEmpty(content_type))
|
||
return null;
|
||
|
||
string clientIP = init.verifyip ? requestInfo.IP : "::1";
|
||
|
||
return await InvokeCache($"videocdn:{content_id}:{content_type}:{accessToken}:{(init.streamproxy ? "" : clientIP)}", 5, async () =>
|
||
{
|
||
var headers = HeadersModel.Init(
|
||
("Authorization", $"Bearer {accessToken}"),
|
||
("User-Agent", HttpContext.Request.Headers.UserAgent)
|
||
);
|
||
|
||
if (!init.streamproxy)
|
||
headers.Add(new("X-LAMPA-CLIENT-IP", clientIP));
|
||
|
||
string json = await httpHydra.Get($"{init.apihost}/stream?clientId={init.clientId}&contentType={content_type}&contentId={content_id}&domain={init.domain}", useDefaultHeaders: false, addheaders: headers);
|
||
if (string.IsNullOrEmpty(json))
|
||
return null;
|
||
|
||
var job = JsonConvert.DeserializeObject<JObject>(json);
|
||
|
||
var model = job["player"].ToObject<EmbedModel>();
|
||
model.tag_url = job["ads"]["rolls"].First.Value<string>("tag_url");
|
||
|
||
return model;
|
||
});
|
||
}
|
||
#endregion
|
||
|
||
#region Search
|
||
async Task<(long content_id, string content_type, SimilarTpl similar)> Search(string imdb_id, long kinopoisk_id, string title, string original_title, int serial, int clarification, bool similar)
|
||
{
|
||
#region database search
|
||
if (similar == false && (!string.IsNullOrEmpty(imdb_id) || kinopoisk_id > 0))
|
||
{
|
||
var item = Lumex.database.FirstOrDefault(i =>
|
||
{
|
||
if (string.IsNullOrEmpty(imdb_id) && i.imdb_id == imdb_id)
|
||
return true;
|
||
|
||
if (kinopoisk_id > 0 && i.kinopoisk_id == kinopoisk_id)
|
||
return true;
|
||
|
||
return false;
|
||
});
|
||
|
||
if (item.id > 0)
|
||
{
|
||
string type = null;
|
||
switch (item.content_type)
|
||
{
|
||
case "tv series":
|
||
case "tv-series":
|
||
case "tv_series":
|
||
case "tvseries":
|
||
type = "tv-series";
|
||
break;
|
||
case "anime tv series":
|
||
case "animetvseries":
|
||
case "anime_tv_series":
|
||
case "anime-tv-series":
|
||
type = "anime-tv-series";
|
||
break;
|
||
case "show tv series":
|
||
case "showtvseries":
|
||
case "show-tv-series":
|
||
case "show_tv_series":
|
||
type = "show-tv-series";
|
||
break;
|
||
default:
|
||
type = item.content_type;
|
||
break;
|
||
}
|
||
|
||
return (item.id, type, default);
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
var movie = similar ? null : (await searchId(imdb_id, 0) ?? await searchId(null, kinopoisk_id));
|
||
if (movie != null)
|
||
{
|
||
return (movie.Value<long>("id"), movie.Value<string>("content_type"), default);
|
||
}
|
||
else
|
||
{
|
||
if (string.IsNullOrEmpty(title ?? original_title) || string.IsNullOrEmpty(init.token))
|
||
return default;
|
||
|
||
string uri = $"{init.iframehost}/api/short?api_token={init.token}&title={HttpUtility.UrlEncode(clarification == 1 ? title : (original_title ?? title))}";
|
||
string json = await httpHydra.Get(uri, safety: true);
|
||
if (json == null)
|
||
return default;
|
||
|
||
SearchRoot root;
|
||
|
||
try
|
||
{
|
||
root = JsonConvert.DeserializeObject<SearchRoot>(json);
|
||
if (root?.data == null || root.data.Length == 0)
|
||
return default;
|
||
}
|
||
catch { return default; }
|
||
|
||
var stpl = new SimilarTpl(root.data.Length);
|
||
|
||
string enc_title = HttpUtility.UrlEncode(title);
|
||
string enc_original_title = HttpUtility.UrlEncode(original_title);
|
||
|
||
int count = 0;
|
||
foreach (var item in root.data)
|
||
{
|
||
if (serial != -1)
|
||
{
|
||
if ((serial == 0 && item.content_type != "movie") || (serial == 1 && item.content_type == "movie"))
|
||
continue;
|
||
}
|
||
|
||
if (clarification != 1)
|
||
{
|
||
bool isok = title != null && title.Length > 3 && item.title != null && item.title.ToLower().Contains(title.ToLower());
|
||
isok = isok ? true : original_title != null && original_title.Length > 3 && item.orig_title != null && item.orig_title.ToLower().Contains(original_title.ToLower());
|
||
|
||
if (!isok)
|
||
continue;
|
||
}
|
||
|
||
string year = item.add?.Split("-")?[0] ?? string.Empty;
|
||
string name = !string.IsNullOrEmpty(item.title) && !string.IsNullOrEmpty(item.orig_title) ? $"{item.title} / {item.orig_title}" : (item.title ?? item.orig_title);
|
||
|
||
string details = $"imdb: {item.imdb_id} {SimilarTpl.OnlineSplit} kinopoisk: {item.kp_id}";
|
||
|
||
string img = PosterApi.Find(item.kp_id, item.imdb_id);
|
||
stpl.Append(name, year, details, $"{host}/lite/videocdn?title={enc_title}&original_title={enc_original_title}&content_id={item.id}&content_type={item.content_type}", img);
|
||
|
||
count += 1;
|
||
}
|
||
|
||
return (0, null, stpl);
|
||
}
|
||
}
|
||
|
||
|
||
async Task<JToken> searchId(string imdb_id, long kinopoisk_id)
|
||
{
|
||
if (string.IsNullOrEmpty(init.token))
|
||
return null;
|
||
|
||
if (string.IsNullOrEmpty(imdb_id) && kinopoisk_id == 0)
|
||
return null;
|
||
|
||
string arg = kinopoisk_id > 0 ? $"&kinopoisk_id={kinopoisk_id}" : $"&imdb_id={imdb_id}";
|
||
var job = await httpHydra.Get<JObject>($"{init.iframehost}/api/short?api_token={init.token}" + arg, safety: true);
|
||
if (job == null || !job.ContainsKey("data"))
|
||
return null;
|
||
|
||
var result = job["data"]?.First;
|
||
if (result == null)
|
||
return null;
|
||
|
||
return result;
|
||
}
|
||
#endregion
|
||
}
|
||
}
|