sepiasearch.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  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: application/json, text/plain, */*",
  148. "Accept-Language: en-US,en;q=0.5",
  149. "Accept-Encoding: gzip, deflate, br, zstd",
  150. "DNT: 1",
  151. "Sec-GPC: 1",
  152. "Connection: keep-alive",
  153. "Referer: https://sepiasearch.org/search",
  154. "Sec-Fetch-Dest: empty",
  155. "Sec-Fetch-Mode: cors",
  156. "Sec-Fetch-Site: same-origin",
  157. "Priority: u=0",
  158. "TE: trailers"]
  159. );
  160. curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
  161. curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
  162. curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
  163. curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
  164. curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
  165. $this->backend->assign_proxy($curlproc, $proxy);
  166. $data = curl_exec($curlproc);
  167. if(curl_errno($curlproc)){
  168. throw new Exception(curl_error($curlproc));
  169. }
  170. curl_close($curlproc);
  171. return $data;
  172. }
  173. public function video($get){
  174. if($get["npt"]){
  175. [$npt, $proxy] =
  176. $this->backend
  177. ->get(
  178. $get["npt"],
  179. "videos"
  180. );
  181. $npt = json_decode($npt, true);
  182. $type = $npt["type"];
  183. $npt = $npt["npt"];
  184. }else{
  185. $proxy = $this->backend->get_ip();
  186. $npt = [
  187. "search" => $get["s"],
  188. "start" => 0,
  189. "count" => 20
  190. ];
  191. if($get["type"] == "videos"){
  192. //
  193. // Parse video filters
  194. //
  195. switch($get["nsfw"]){
  196. case "yes": $npt["nsfw"] = "both"; break;
  197. case "no": $npt["nsfw"] = "false"; break;
  198. }
  199. $npt["boostLanguages[]"] = "en";
  200. if($get["language"] != "any"){
  201. $npt["languageOneOf[]"] = $get["language"];
  202. }
  203. if($get["sort"] != "best"){
  204. $npt["sort"] = $get["sort"];
  205. }
  206. if($get["newer"] !== false){
  207. $date = new DateTime("@{$get["newer"]}");
  208. $date->setTimezone(new DateTimeZone("UTC"));
  209. $formatted = $date->format("Y-m-d\TH:i:s.000\Z");
  210. $npt["startDate"] = $formatted;
  211. }
  212. switch($get["duration"]){
  213. case "short":
  214. $npt["durationMax"] = 240;
  215. break;
  216. case "medium":
  217. $npt["durationMin"] = 240;
  218. $npt["durationMax"] = 600;
  219. break;
  220. case "long":
  221. $npt["durationMin"] = 600;
  222. break;
  223. }
  224. if($get["category"] != "any"){
  225. $npt["categoryOneOf[]"] = $get["category"];
  226. }
  227. if($get["display"] != "any"){
  228. $npt["isLive"] = $get["display"];
  229. }
  230. if($get["license"] != "any"){
  231. // typo in license, lol
  232. $npt["licenceOneOf[]"] = $get["license"];
  233. }
  234. }
  235. $type = $get["type"];
  236. }
  237. switch($type){
  238. case "videos":
  239. $url = "https://sepiasearch.org/api/v1/search/videos";
  240. break;
  241. case "channels":
  242. $url = "https://sepiasearch.org/api/v1/search/video-channels";
  243. break;
  244. case "playlists":
  245. $url = "https://sepiasearch.org/api/v1/search/video-playlists";
  246. break;
  247. }
  248. //$json = file_get_contents("scraper/sepia.json");
  249. try{
  250. $json =
  251. $this->get(
  252. $proxy,
  253. $url,
  254. $npt
  255. );
  256. }catch(Exception $error){
  257. throw new Exception("Failed to fetch JSON");
  258. }
  259. $json = json_decode($json, true);
  260. if($json === null){
  261. throw new Exception("Failed to parse JSON");
  262. }
  263. if(isset($json["errors"])){
  264. $msg = [];
  265. foreach($json["errors"] as $error){
  266. if(isset($error["msg"])){
  267. $msg[] = $error["msg"];
  268. }
  269. }
  270. throw new Exception("Sepia Search returned error(s): " . implode(", ", $msg));
  271. }
  272. if(!isset($json["data"])){
  273. throw new Exception("Sepia Search did not return a data object");
  274. }
  275. $out = [
  276. "status" => "ok",
  277. "npt" => null,
  278. "video" => [],
  279. "author" => [],
  280. "livestream" => [],
  281. "playlist" => [],
  282. "reel" => []
  283. ];
  284. switch($get["type"]){
  285. case "videos":
  286. foreach($json["data"] as $video){
  287. if(count($video["account"]["avatars"]) !== 0){
  288. $avatar =
  289. $video["account"]["avatars"][count($video["account"]["avatars"]) - 1]["url"];
  290. }else{
  291. $avatar = null;
  292. }
  293. if($video["thumbnailUrl"] === null){
  294. $thumb = [
  295. "ratio" => null,
  296. "url" => null
  297. ];
  298. }else{
  299. $thumb = [
  300. "ratio" => "16:9",
  301. "url" => $video["thumbnailUrl"]
  302. ];
  303. }
  304. if($video["isLive"]){
  305. $append = "livestream";
  306. }else{
  307. $append = "video";
  308. }
  309. $out[$append][] = [
  310. "title" => $video["name"],
  311. "description" =>
  312. $this->limitstrlen(
  313. $this->titledots(
  314. $video["description"]
  315. )
  316. ),
  317. "author" => [
  318. "name" => $video["account"]["displayName"] . " ({$video["account"]["name"]})",
  319. "url" => $video["account"]["url"],
  320. "avatar" => $avatar
  321. ],
  322. "date" => strtotime($video["publishedAt"]),
  323. "duration" => $video["isLive"] ? "_LIVE" : $video["duration"],
  324. "views" => $video["views"],
  325. "thumb" => $thumb,
  326. "url" => $video["url"]
  327. ];
  328. }
  329. break;
  330. case "playlists":
  331. foreach($json["data"] as $playlist){
  332. if(count($playlist["ownerAccount"]["avatars"]) !== 0){
  333. $avatar =
  334. $playlist["ownerAccount"]["avatars"][count($playlist["ownerAccount"]["avatars"]) - 1]["url"];
  335. }else{
  336. $avatar = null;
  337. }
  338. if($playlist["thumbnailUrl"] === null){
  339. $thumb = [
  340. "ratio" => null,
  341. "url" => null
  342. ];
  343. }else{
  344. $thumb = [
  345. "ratio" => "16:9",
  346. "url" => $playlist["thumbnailUrl"]
  347. ];
  348. }
  349. $out["playlist"][] = [
  350. "title" => $playlist["displayName"],
  351. "description" =>
  352. $this->limitstrlen(
  353. $this->titledots(
  354. $playlist["description"]
  355. )
  356. ),
  357. "author" => [
  358. "name" => $playlist["ownerAccount"]["displayName"] . " ({$playlist["ownerAccount"]["name"]})",
  359. "url" => $playlist["ownerAccount"]["url"],
  360. "avatar" => $avatar
  361. ],
  362. "date" => strtotime($playlist["createdAt"]),
  363. "duration" => $playlist["videosLength"],
  364. "views" => null,
  365. "thumb" => $thumb,
  366. "url" => $playlist["url"]
  367. ];
  368. }
  369. break;
  370. case "channels":
  371. foreach($json["data"] as $channel){
  372. if(count($channel["avatars"]) !== 0){
  373. $thumb = [
  374. "ratio" => "1:1",
  375. "url" => $channel["avatars"][count($channel["avatars"]) - 1]["url"]
  376. ];
  377. }else{
  378. $thumb = [
  379. "ratio" => null,
  380. "url" => null
  381. ];
  382. }
  383. $out["author"][] = [
  384. "title" => $channel["displayName"] . " ({$channel["name"]})",
  385. "followers" => $channel["followersCount"],
  386. "description" =>
  387. $channel["videosCount"] . " videos. " .
  388. $this->limitstrlen(
  389. $this->titledots(
  390. $channel["description"]
  391. )
  392. ),
  393. "thumb" => $thumb,
  394. "url" => $channel["url"]
  395. ];
  396. }
  397. break;
  398. }
  399. // get next page
  400. if($json["total"] - 20 > $npt["start"]){
  401. $npt["start"] += 20;
  402. $npt = [
  403. "type" => $get["type"],
  404. "npt" => $npt
  405. ];
  406. $out["npt"] =
  407. $this->backend
  408. ->store(
  409. json_encode($npt),
  410. "videos",
  411. $proxy
  412. );
  413. }
  414. return $out;
  415. }
  416. private function titledots($title){
  417. $substr = substr($title, -3);
  418. if(
  419. $substr == "..." ||
  420. $substr == "…"
  421. ){
  422. return trim(substr($title, 0, -3), " \n\r\t\v\x00\0\x0B\xc2\xa0");
  423. }
  424. return trim($title, " \n\r\t\v\x00\0\x0B\xc2\xa0");
  425. }
  426. private function limitstrlen($text){
  427. return
  428. explode(
  429. "\n",
  430. wordwrap(
  431. str_replace(
  432. ["\n\r", "\r\n", "\n", "\r"],
  433. " ",
  434. $text
  435. ),
  436. 300,
  437. "\n"
  438. ),
  439. 2
  440. )[0];
  441. }
  442. }