sepiasearch.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. <?php
  2. class sepiasearch{
  3. public function __construct(){
  4. include "lib/backend.php";
  5. $this->backend = new backend("sepiasearch");
  6. }
  7. public function getfilters($page){
  8. return [
  9. "nsfw" => [
  10. "display" => "NSFW",
  11. "option" => [
  12. "yes" => "Yes", // &sensitiveContent=both
  13. "no" => "No" // &sensitiveContent=false
  14. ]
  15. ],
  16. "language" => [
  17. "display" => "Language", // &language=
  18. "option" => [
  19. "any" => "Any language",
  20. "en" => "English",
  21. "fr" => "Français",
  22. "ar" => "العربية",
  23. "ca" => "Català",
  24. "cs" => "Čeština",
  25. "de" => "Deutsch",
  26. "el" => "ελληνικά",
  27. "eo" => "Esperanto",
  28. "es" => "Español",
  29. "eu" => "Euskara",
  30. "fa" => "فارسی",
  31. "fi" => "Suomi",
  32. "gd" => "Gàidhlig",
  33. "gl" => "Galego",
  34. "hr" => "Hrvatski",
  35. "hu" => "Magyar",
  36. "is" => "Íslenska",
  37. "it" => "Italiano",
  38. "ja" => "日本語",
  39. "kab" => "Taqbaylit",
  40. "nl" => "Nederlands",
  41. "no" => "Norsk",
  42. "oc" => "Occitan",
  43. "pl" => "Polski",
  44. "pt" => "Português (Brasil)",
  45. "pt-PT" => "Português (Portugal)",
  46. "ru" => "Pусский",
  47. "sk" => "Slovenčina",
  48. "sq" => "Shqip",
  49. "sv" => "Svenska",
  50. "th" => "ไทย",
  51. "tok" => "Toki Pona",
  52. "tr" => "Türkçe",
  53. "uk" => "украї́нська мо́ва",
  54. "vi" => "Tiếng Việt",
  55. "zh-Hans" => "简体中文(中国)",
  56. "zh-Hant" => "繁體中文(台灣)"
  57. ]
  58. ],
  59. "type" => [
  60. "display" => "Result type", // i handle this
  61. "option" => [
  62. "videos" => "Videos",
  63. "playlists" => "Playlists",
  64. "channels" => "Channels"
  65. ]
  66. ],
  67. "sort" => [
  68. "display" => "Sort by",
  69. "option" => [
  70. "best" => "Best match", // no filter
  71. "-publishedAt" => "Newest", // sort=-publishedAt
  72. "publishedAt" => "Oldest" // sort=publishedAt
  73. ]
  74. ],
  75. "newer" => [ // &startDate=2025-07-26T04:00:00.000Z
  76. "display" => "Newer than",
  77. "option" => "_DATE"
  78. ],
  79. "duration" => [
  80. "display" => "Duration",
  81. "option" => [
  82. "any" => "Any duration",
  83. "short" => "Short (0-4mins)", // &durationRange=short
  84. "medium" => "Medium (4-10 mins)",
  85. "long" => "Long (10+ mins)",
  86. ]
  87. ],
  88. "category" => [
  89. "display" => "Category", // &categoryOneOf[]=
  90. "option" => [
  91. "any" => "Any category",
  92. "1" => "Music",
  93. "2" => "Films",
  94. "3" => "Vehicles",
  95. "4" => "Art",
  96. "5" => "Sports",
  97. "6" => "Travels",
  98. "7" => "Gaming",
  99. "8" => "People",
  100. "9" => "Comedy",
  101. "10" => "Entertainment",
  102. "11" => "News & Politics",
  103. "12" => "How To",
  104. "13" => "Education",
  105. "14" => "Activism",
  106. "15" => "Science & Technology",
  107. "16" => "Animals",
  108. "17" => "Kids",
  109. "18" => "Food"
  110. ]
  111. ],
  112. "display" => [
  113. "display" => "Display",
  114. "option" => [
  115. "any" => "Everything",
  116. "true" => "Live videos", // &isLive=true
  117. "false" => "VODs" // &isLive=false
  118. ]
  119. ],
  120. "license" => [
  121. "display" => "License", // &license=
  122. "option" => [
  123. "any" => "Any license",
  124. "1" => "Attribution",
  125. "2" => "Attribution - Share Alike",
  126. "3" => "Attribution - No Derivatives",
  127. "4" => "Attribution - Non Commercial",
  128. "5" => "Attribution - Non Commercial - Share Alike",
  129. "6" => "Attribution - Non Commercial - No Derivatives",
  130. "7" => "Public Domain Dedication"
  131. ]
  132. ]
  133. ];
  134. }
  135. private function get($proxy, $url, $get = []){
  136. $curlproc = curl_init();
  137. if($get !== []){
  138. $get = http_build_query($get);
  139. $url .= "?" . $get;
  140. }
  141. curl_setopt($curlproc, CURLOPT_URL, $url);
  142. curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
  143. curl_setopt(
  144. $curlproc,
  145. CURLOPT_HTTPHEADER,
  146. ["User-Agent: " . config::USER_AGENT,
  147. "Accept: */*",
  148. "Accept-Language: en-US,en;q=0.5",
  149. "Accept-Encoding: gzip",
  150. "Cookie: PREF=tz=America.New_York",
  151. "Referer: https://youtube.com.com/",
  152. "Content-Type: application/json",
  153. "DNT: 1",
  154. "Connection: keep-alive",
  155. "Sec-Fetch-Dest: empty",
  156. "Sec-Fetch-Mode: same-origin",
  157. "Sec-Fetch-Site: same-origin"]
  158. );
  159. curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
  160. curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
  161. curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
  162. curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
  163. curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
  164. $this->backend->assign_proxy($curlproc, $proxy);
  165. $data = curl_exec($curlproc);
  166. if(curl_errno($curlproc)){
  167. throw new Exception(curl_error($curlproc));
  168. }
  169. curl_close($curlproc);
  170. return $data;
  171. }
  172. public function video($get){
  173. if($get["npt"]){
  174. [$npt, $proxy] =
  175. $this->backend
  176. ->get(
  177. $get["npt"],
  178. "videos"
  179. );
  180. $npt = json_decode($npt, true);
  181. $type = $npt["type"];
  182. $npt = $npt["npt"];
  183. }else{
  184. $proxy = $this->backend->get_ip();
  185. $npt = [
  186. "search" => $get["s"],
  187. "start" => 0,
  188. "count" => 20
  189. ];
  190. if($get["type"] == "videos"){
  191. //
  192. // Parse video filters
  193. //
  194. switch($get["nsfw"]){
  195. case "yes": $npt["nsfw"] = "both"; break;
  196. case "no": $npt["nsfw"] = "false"; break;
  197. }
  198. $npt["boostLanguages[]"] = "en";
  199. if($get["language"] != "any"){
  200. $npt["languageOneOf[]"] = $get["language"];
  201. }
  202. if($get["sort"] != "best"){
  203. $npt["sort"] = $get["sort"];
  204. }
  205. if($get["newer"] !== false){
  206. $date = new DateTime("@{$get["newer"]}");
  207. $date->setTimezone(new DateTimeZone("UTC"));
  208. $formatted = $date->format("Y-m-d\TH:i:s.000\Z");
  209. $npt["startDate"] = $formatted;
  210. }
  211. switch($get["duration"]){
  212. case "short":
  213. $npt["durationMax"] = 240;
  214. break;
  215. case "medium":
  216. $npt["durationMin"] = 240;
  217. $npt["durationMax"] = 600;
  218. break;
  219. case "long":
  220. $npt["durationMin"] = 600;
  221. break;
  222. }
  223. if($get["category"] != "any"){
  224. $npt["categoryOneOf[]"] = $get["category"];
  225. }
  226. if($get["display"] != "any"){
  227. $npt["isLive"] = $get["display"];
  228. }
  229. if($get["license"] != "any"){
  230. // typo in license, lol
  231. $npt["licenceOneOf[]"] = $get["license"];
  232. }
  233. }
  234. $type = $get["type"];
  235. }
  236. switch($type){
  237. case "videos":
  238. $url = "https://sepiasearch.org/api/v1/search/videos";
  239. break;
  240. case "channels":
  241. $url = "https://sepiasearch.org/api/v1/search/video-channels";
  242. break;
  243. case "playlists":
  244. $url = "https://sepiasearch.org/api/v1/search/video-playlists";
  245. break;
  246. }
  247. //$json = file_get_contents("scraper/sepia.json");
  248. try{
  249. $json =
  250. $this->get(
  251. $proxy,
  252. $url,
  253. $npt
  254. );
  255. }catch(Exception $error){
  256. throw new Exception("Failed to fetch JSON");
  257. }
  258. $json = json_decode($json, true);
  259. if($json === null){
  260. throw new Exception("Failed to parse JSON");
  261. }
  262. if(isset($json["errors"])){
  263. $msg = [];
  264. foreach($json["errors"] as $error){
  265. if(isset($error["msg"])){
  266. $msg[] = $error["msg"];
  267. }
  268. }
  269. throw new Exception("Sepia Search returned error(s): " . implode(", ", $msg));
  270. }
  271. if(!isset($json["data"])){
  272. throw new Exception("Sepia Search did not return a data object");
  273. }
  274. $out = [
  275. "status" => "ok",
  276. "npt" => null,
  277. "video" => [],
  278. "author" => [],
  279. "livestream" => [],
  280. "playlist" => [],
  281. "reel" => []
  282. ];
  283. switch($get["type"]){
  284. case "videos":
  285. foreach($json["data"] as $video){
  286. if(count($video["account"]["avatars"]) !== 0){
  287. $avatar =
  288. $video["account"]["avatars"][count($video["account"]["avatars"]) - 1]["url"];
  289. }else{
  290. $avatar = null;
  291. }
  292. if($video["thumbnailUrl"] === null){
  293. $thumb = [
  294. "ratio" => null,
  295. "url" => null
  296. ];
  297. }else{
  298. $thumb = [
  299. "ratio" => "16:9",
  300. "url" => $video["thumbnailUrl"]
  301. ];
  302. }
  303. if($video["isLive"]){
  304. $append = "livestream";
  305. }else{
  306. $append = "video";
  307. }
  308. $out[$append][] = [
  309. "title" => $video["name"],
  310. "description" =>
  311. $this->limitstrlen(
  312. $this->titledots(
  313. $video["description"]
  314. )
  315. ),
  316. "author" => [
  317. "name" => $video["account"]["displayName"] . " ({$video["account"]["name"]})",
  318. "url" => $video["account"]["url"],
  319. "avatar" => $avatar
  320. ],
  321. "date" => strtotime($video["publishedAt"]),
  322. "duration" => $video["isLive"] ? "_LIVE" : $video["duration"],
  323. "views" => $video["views"],
  324. "thumb" => $thumb,
  325. "url" => $video["url"]
  326. ];
  327. }
  328. break;
  329. case "playlists":
  330. foreach($json["data"] as $playlist){
  331. if(count($playlist["ownerAccount"]["avatars"]) !== 0){
  332. $avatar =
  333. $playlist["ownerAccount"]["avatars"][count($playlist["ownerAccount"]["avatars"]) - 1]["url"];
  334. }else{
  335. $avatar = null;
  336. }
  337. if($playlist["thumbnailUrl"] === null){
  338. $thumb = [
  339. "ratio" => null,
  340. "url" => null
  341. ];
  342. }else{
  343. $thumb = [
  344. "ratio" => "16:9",
  345. "url" => $playlist["thumbnailUrl"]
  346. ];
  347. }
  348. $out["playlist"][] = [
  349. "title" => $playlist["displayName"],
  350. "description" =>
  351. $this->limitstrlen(
  352. $this->titledots(
  353. $playlist["description"]
  354. )
  355. ),
  356. "author" => [
  357. "name" => $playlist["ownerAccount"]["displayName"] . " ({$playlist["ownerAccount"]["name"]})",
  358. "url" => $playlist["ownerAccount"]["url"],
  359. "avatar" => $avatar
  360. ],
  361. "date" => strtotime($playlist["createdAt"]),
  362. "duration" => $playlist["videosLength"],
  363. "views" => null,
  364. "thumb" => $thumb,
  365. "url" => $playlist["url"]
  366. ];
  367. }
  368. break;
  369. case "channels":
  370. foreach($json["data"] as $channel){
  371. if(count($channel["avatars"]) !== 0){
  372. $thumb = [
  373. "ratio" => "1:1",
  374. "url" => $channel["avatars"][count($channel["avatars"]) - 1]["url"]
  375. ];
  376. }else{
  377. $thumb = [
  378. "ratio" => null,
  379. "url" => null
  380. ];
  381. }
  382. $out["author"][] = [
  383. "title" => $channel["displayName"] . " ({$channel["name"]})",
  384. "followers" => $channel["followersCount"],
  385. "description" =>
  386. $channel["videosCount"] . " videos. " .
  387. $this->limitstrlen(
  388. $this->titledots(
  389. $channel["description"]
  390. )
  391. ),
  392. "thumb" => $thumb,
  393. "url" => $channel["url"]
  394. ];
  395. }
  396. break;
  397. }
  398. // get next page
  399. if($json["total"] - 20 > $npt["start"]){
  400. $npt["start"] += 20;
  401. $npt = [
  402. "type" => $get["type"],
  403. "npt" => $npt
  404. ];
  405. $out["npt"] =
  406. $this->backend
  407. ->store(
  408. json_encode($npt),
  409. "videos",
  410. $proxy
  411. );
  412. }
  413. return $out;
  414. }
  415. private function titledots($title){
  416. $substr = substr($title, -3);
  417. if(
  418. $substr == "..." ||
  419. $substr == "…"
  420. ){
  421. return trim(substr($title, 0, -3), " \n\r\t\v\x00\0\x0B\xc2\xa0");
  422. }
  423. return trim($title, " \n\r\t\v\x00\0\x0B\xc2\xa0");
  424. }
  425. private function limitstrlen($text){
  426. return
  427. explode(
  428. "\n",
  429. wordwrap(
  430. str_replace(
  431. ["\n\r", "\r\n", "\n", "\r"],
  432. " ",
  433. $text
  434. ),
  435. 300,
  436. "\n"
  437. ),
  438. 2
  439. )[0];
  440. }
  441. }