remove 44 commits across 4 months to remove shit like secrets, api keys etc from private groups and trackers

This commit is contained in:
ThaUnknown 2022-03-08 20:18:13 +01:00
parent 477a88418c
commit 8fb0bf4117
19 changed files with 506 additions and 84979 deletions

View file

@ -1,3 +1,11 @@
# THIS IS AN ARCHIVE BRANCH, IT WAS PRIVATE FOR A REASON
This branch is entirely for the sake of "I worked on it, I want the commits", it was private because the code was dogshit and honestly a joke, and it's being re-written from scratch for a good reason!!!
This includes meme commits, testing on production, write your own JS framework and other blantant "don't do you fucking idiot", read the code/commits at your own risk!
<br><br>
[![forthebadge](https://forthebadge.com/images/badges/it-works-why.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/powered-by-black-magic.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/works-on-my-machine.svg)](https://forthebadge.com)
# Miru
Miru is a simple anime torrent streaming client targetted at normies, meant as a replacement for shitty streaming sites with a lot of extra added functionality.

View file

@ -17,6 +17,18 @@ body {
user-select: none;
}
noscript {
width: 100%;
height: 100%;
position: absolute;
background: #000;
z-index: 1000;
}
#gsignin {
background: #000;
}
.badge-color {
background-color: var(--color) !important;
border-color: var(--color) !important;

View file

@ -13,7 +13,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
<meta property="og:title" content="Miru">
<meta property="og:url" content="">
<meta property="og:url" content="https://miru.pages.dev/">
<meta property="og:description" content="Miru - Torrent streaming made simple!">
<meta property="og:type" content="video.other">
<meta property="og:image" content="logo.png">
@ -26,6 +26,16 @@
</head>
<body class="dark-mode with-custom-webkit-scrollbars with-custom-css-scrollbars" data-sidebar-shortcut-enabled="true">
<noscript>
<div class="d-flex align-items-center justify-content-center h-full w-full font-size-30 font-weight-bold">
What are you fucking stupid? Enable JavaScript.
</div>
</noscript>
<div id="gsignin" class="w-full h-full z-50 position-absolute d-none">
<div class="w-full h-full d-flex align-items-center justify-content-center font-size-30 font-weight-bold text-center">
Click to sign in with your Google Drive account.<br> Make sure you grant GDrive access!
</div>
</div>
<div class="modal modal-full" id="viewAnime" tabindex="-1" role="dialog">
<div class="h-full modal-content bg-very-dark p-0 overflow-y-auto">
<button class="close pointer z-30 bg-dark shadow-lg border" data-dismiss="modal" type="button" aria-label="Close">
@ -155,16 +165,16 @@
<h1 class="title font-weight-bold text-white">Sypnosis</h1>
<div class="font-size-16 pr-15">
</div>
<h1 class="title font-weight-bold text-white pt-20">Episodes</h1>
<div class="d-flex flex-wrap justify-content-start">
<!-- <div class="position-relative w-250 rounded mr-10 mb-10 overflow-hidden pointer">
<!-- <h1 class="title font-weight-bold text-white pt-20">Episodes</h1>
<div class="d-flex flex-wrap justify-content-start"> -->
<!-- <div class="position-relative w-250 rounded mr-10 mb-10 overflow-hidden pointer">
<img loading="lazy"
src="https://img1.ak.crunchyroll.com/i/spire1-tmb/b199406edeebc19a7f4e4412d6e1dfcc1365964779_full.jpg"
class="w-full h-full">
<div class="position-absolute ep-title w-full p-5 text-truncate bottom-0">Episode 1 - To You, 2,000
Years in the Future -The Fall of Zhiganshina (1)</div>
</div> -->
</div>
<!-- </div> -->
</div>
<div class="col-md-3 px-sm-0 px-20">
<h1 class="title font-weight-bold text-white">Details</h1>
@ -896,12 +906,8 @@ wss://peertube.cpy.re:443/tracker/socket</textarea>
<input id="subtitle1" type="text" list="subtitle1list" class="form-control form-control-lg"
autocomplete="off" value="SubsPlease">
<datalist id="subtitle1list">
<option value="SubsPlease">Roboto
Medium,26,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,1.3,0,2,20,20,23,1
</option>
<option value="Erai-raws">Open Sans
Semibold,45,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,10,10,25,1
</option>
<option value="SubsPlease">Roboto Medium,26,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,1.3,0,2,20,20,23,1</option>
<option value="Erai-raws">Open Sans Semibold,45,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,10,10,25,1</option>
</datalist>
</div>
<div class="input-group w-300 mb-10 form-control-lg" data-toggle="tooltip" data-placement="top"
@ -942,7 +948,7 @@ wss://peertube.cpy.re:443/tracker/socket</textarea>
<label for="player6">Autoplay Next Episode</label>
</div>
<div class="custom-switch mb-10 pl-10 font-size-16 w-300" data-toggle="tooltip" data-placement="top"
data-title="Pauses/Resumes Video Playback When Tabbing In/Out Of The App.">
data-title="Pauses/Resumes Video Playback When Tabbing In/Out Of The App">
<input type="checkbox" id="player10" checked>
<label for="player10">Pause When Tabbing Out</label>
</div>
@ -961,8 +967,8 @@ wss://peertube.cpy.re:443/tracker/socket</textarea>
<input id="torrent4" type="text" list="torrent4list" class="form-control form-control-lg"
autocomplete="off" value="SubsPlease">
<datalist id="torrent4list">
<option value="SubsPlease">https://subsplease.org/rss/?r=</option>
<option value="Erai-raws">https://www.erai-raws.info/rss-</option>
<option value="SubsPlease">https://meowinjapanese.cf/?page=rss&c=0_0&f=0&u=subsplease&q=</option>
<option value="Erai-raws">https://meowinjapanese.cf/?page=rss&c=0_0&f=0&u=Erai-raws&q=</option>
</datalist>
</div>
<div class="input-group mb-10 w-300 form-control-lg" data-toggle="tooltip" data-placement="top"
@ -971,9 +977,9 @@ wss://peertube.cpy.re:443/tracker/socket</textarea>
<span class="input-group-text w-100 justify-content-center">Quality</span>
</div>
<select class="form-control form-control-lg" id="torrent1">
<option value="1080" selected>1080p</option>
<option value="720">720p</option>
<option value='480"||"540'>SD</option>
<option value='"1080"' selected>1080p</option>
<option value='"720"'>720p</option>
<option value='"480"||"540"'>SD</option>
</select>
</div>
<break></break>
@ -998,6 +1004,11 @@ wss://peertube.cpy.re:443/tracker/socket</textarea>
<input type="checkbox" id="torrent9">
<label for="torrent9">Batch Lookup</label>
</div>
<div class="custom-switch mb-10 pl-10 font-size-16 w-300" data-toggle="tooltip" data-placement="top"
data-title="Shows All Anime, Even Ones That Can't Be Played Back">
<input type="checkbox" id="other3">
<label for="other2">Show All Anime</label>
</div>
</div>
</div>
<div class="content my-10">

View file

@ -1,4 +1,5 @@
import { alID } from './interface.js'
import { settings } from './settings.js'
import halfmoon from 'halfmoon'
async function handleRequest (opts) {
@ -71,7 +72,8 @@ export async function alRequest (opts) {
perPage: opts.perPage || 30,
status_in: opts.status_in || '[CURRENT,PLANNING]',
chunk: opts.chunk || 1,
perchunk: opts.perChunk || 30
perchunk: opts.perChunk || 30,
startDate: (!settings.other3 && (opts.startDate || 20210328)) || 10000000
}
const options = {
method: 'POST',
@ -260,12 +262,12 @@ query ($page: Int, $perPage: Int, $from: Int, $to: Int) {
variables.status = opts.status
variables.sort = opts.sort || 'SEARCH_MATCH'
query = `
query ($page: Int, $perPage: Int, $sort: [MediaSort], $type: MediaType, $search: String, $status: MediaStatus, $season: MediaSeason, $year: Int, $genre: String, $format: MediaFormat) {
query ($page: Int, $perPage: Int, $sort: [MediaSort], $type: MediaType, $search: String, $status: MediaStatus, $season: MediaSeason, $year: Int, $genre: String, $format: MediaFormat, $startDate: FuzzyDateInt) {
Page (page: $page, perPage: $perPage) {
pageInfo {
hasNextPage
},
media(type: $type, search: $search, sort: $sort, status: $status, season: $season, seasonYear: $year, genre: $genre, format: $format) {
media(type: $type, search: $search, sort: $sort, status: $status, season: $season, seasonYear: $year, genre: $genre, format: $format, startDate_greater: $startDate) {
${queryObjects}
}
}

View file

@ -111,14 +111,8 @@ export async function resolveFileMedia (opts) {
async function resolveTitle (title) {
if (!(title in relations)) {
// resolve name and shit
let method, res
if (opts.isRelease) {
method = { name: title, method: 'SearchName', perPage: 1, status: ['RELEASING'], sort: 'SEARCH_MATCH' }
// maybe releases should include this and last season? idfk
} else {
method = { name: title, method: 'SearchName', perPage: 1, status: ['RELEASING', 'FINISHED'], sort: 'SEARCH_MATCH' }
}
res = await alRequest(method)
const method = { name: title, method: 'SearchName', perPage: 1, status: ['RELEASING', 'FINISHED'], sort: 'SEARCH_MATCH', startDate: 10000000 }
let res = await alRequest(method)
if (!res.data.Page.media[0]) {
const index = method.name.search(/S\d/)
method.name = ((index !== -1 && method.name.slice(0, index) + method.name.slice(index + 1, method.name.length)) || method.name).replace('(TV)', '').replace(/ (19[5-9]\d|20[0-6]\d)/, '').replace('-', '')
@ -138,7 +132,11 @@ export async function resolveFileMedia (opts) {
const parseObjs = await Promise.all(parsePromises)
await Promise.all([...new Set(parseObjs.map(obj => obj.anime_title))].map(title => resolveTitle(title)))
const assoc = {}
for (const media of (await alRequest({ method: 'SearchIDS', id: [...new Set(parseObjs.map(obj => relations[obj.anime_title]))], perPage: 50 })).data.Page.media) assoc[media.id] = media
for (let ids = [...new Set(parseObjs.map(obj => relations[obj.anime_title]))]; ids.length; ids = ids.slice(50)) {
for await (const media of (await alRequest({ method: 'SearchIDS', id: ids.slice(0, 50), perPage: 50 })).data.Page.media) {
assoc[media.id] = media
}
}
const fileMedias = []
for (const praseObj of parseObjs) {
let episode
@ -161,16 +159,16 @@ export async function resolveFileMedia (opts) {
tempMedia = opts.media.relations.edges.filter(edge => edge.relationType === 'SEQUEL' && (edge.node.format === 'TV' || 'TV_SHORT'))[0].node
increment = true
}
if (tempMedia?.episodes && epMax - (opts.offset + tempMedia.episodes) > (media.nextAiringEpisode?.episode || media.episodes)) {
if (tempMedia?.episodes && epMax - (opts.offset + media.episodes) > (media.nextAiringEpisode?.episode || media.episodes)) {
// episode is still out of bounds
const nextEdge = await alRequest({ method: 'SearchIDSingle', id: tempMedia.id })
await resolveSeason({ media: nextEdge.data.Media, episode: opts.episode, offset: opts.offset + nextEdge.data.Media.episodes, increment: increment })
} else if (tempMedia?.episodes && epMax - (opts.offset + tempMedia.episodes) <= (media.nextAiringEpisode?.episode || media.episodes) && epMin - (opts.offset + tempMedia.episodes) > 0) {
} else if (tempMedia?.episodes && epMax - (opts.offset + media.episodes) <= (media.nextAiringEpisode?.episode || media.episodes) && epMin - (opts.offset + media.episodes) > 0) {
// episode is in range, seems good! overwriting media to count up "seasons"
if (opts.episode.constructor === Array) {
episode = `${praseObj.episode_number[0] - (opts.offset + tempMedia.episodes)} ~ ${praseObj.episode_number[praseObj.episode_number.length - 1] - (opts.offset + tempMedia.episodes)}`
episode = `${praseObj.episode_number[0] - (opts.offset + media.episodes)} ~ ${praseObj.episode_number[praseObj.episode_number.length - 1] - (opts.offset + media.episodes)}`
} else {
episode = opts.episode - (opts.offset + tempMedia.episodes)
episode = opts.episode - (opts.offset + media.episodes)
}
if (opts.increment || increment) {
const nextEdge = await alRequest({ method: 'SearchIDSingle', id: tempMedia.id })

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,76 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
* @license MIT
*/
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*!
* range-parser
* Copyright(c) 2012-2014 TJ Holowaychuk
* Copyright(c) 2015-2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*! bittorrent-protocol. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
/*! blob-to-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! cache-chunk-store. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! create-torrent. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
/*! https://mths.be/punycode v1.3.2 by @mathias */
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! immediate-chunk-store. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! lt_donthave. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
/*! magnet-uri. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
/*! mediasource. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! multistream. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! parse-torrent. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
/*! queue-microtask. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! render-media. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! run-parallel-limit. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! run-parallel. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! simple-concat. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! simple-get. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! simple-peer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! simple-websocket. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! stream-to-blob-url. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! stream-to-blob. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! stream-with-known-length-to-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! torrent-discovery. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
/*! torrent-piece. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
/*! ut_metadata. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
/*! webtorrent. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */

File diff suppressed because one or more lines are too long

View file

@ -7,7 +7,7 @@ import { resolveFileMedia, relations, nyaaSearch } from './anime.js'
import { getRSSurl, getRSSContent } from './rss.js'
import { settings } from './settings.js'
import { client } from './main.js'
import { countdown, flattenObj } from './util.js'
import { countdown, flattenObj, userBrowser } from './util.js'
import halfmoon from 'halfmoon'
export function loadHomePage () {
const homeLoadElements = [navSchedule, homeContinueMore, homeReleasesMore, homePlanningMore, homeTrendingMore, homeRomanceMore, homeActionMore]
@ -19,7 +19,7 @@ export function loadHomePage () {
if (!page) gallerySkeleton(browseGallery)
const res = await alRequest({ method: 'UserLists', status_in: 'CURRENT', id: alID, page: page || 1 })
const mediaList = res.data.Page.mediaList.map(i => i.media).filter(media => {
return !(media.status === 'RELEASING' && media.mediaListEntry?.progress === media.nextAiringEpisode?.episode - 1)
return media.status !== 'RELEASING' || media.mediaListEntry?.progress < media.nextAiringEpisode?.episode - 1
})
galleryAppend({ media: mediaList, gallery: browseGallery, method: 'continue', page: page || 1 })
return res.data.Page.pageInfo.hasNextPage
@ -94,7 +94,7 @@ export function loadHomePage () {
continue: function () {
alRequest({ method: 'UserLists', status_in: 'CURRENT', id: alID, perPage: 50 }).then(res => {
const mediaList = res.data.Page.mediaList.filter(({ media }) => {
return !(media.status === 'RELEASING' && media.mediaListEntry?.progress === media.nextAiringEpisode?.episode - 1)
return media.status !== 'RELEASING' || media.mediaListEntry?.progress < media.nextAiringEpisode?.episode - 1
}).slice(0, 5).map(i => i.media)
galleryAppend({ media: mediaList, gallery: homeContinue })
})
@ -305,6 +305,7 @@ function genreBadges (genres = []) {
return badges
}
const detailsMap = [
{ property: 'episode', label: 'Airing', icon: 'schedule', custom: 'property' },
{ property: 'genres', label: 'Genres', icon: 'theater_comedy' },
{ property: 'season', label: 'Season', icon: 'spa', custom: 'property' },
{ property: 'episodes', label: 'Episodes', icon: 'theaters', custom: 'property' },
@ -316,7 +317,7 @@ const detailsMap = [
{ property: 'averageScore', label: 'Rating', icon: 'trending_up', custom: 'property' },
{ property: 'english', label: 'English', icon: 'title' },
{ property: 'romaji', label: 'Romaji', icon: 'translate' },
{ property: 'native', label: 'Native', icon: '日本', custom: 'icon' }
{ property: 'native', label: 'Native', icon: '', custom: 'icon' }
]
/* global detailTemplate */
const detailTemp = detailTemplate.cloneNode(true).content
@ -358,6 +359,8 @@ function mediaDetails (media) {
nodes[4].textContent = media.averageScore + '%'
} else if (detail.property === 'season') {
nodes[4].textContent = [media.season?.toLowerCase(), media.seasonYear].filter(f => f).join(' ')
} else if (detail.property === 'episode') {
nodes[4].textContent = `Ep ${media.episode}: ${countdown(media.timeUntilAiring)}`
} else {
nodes[4].textContent = media[detail.property]
}
@ -425,8 +428,8 @@ export function viewMedia (input, episode) {
viewNodes[63].innerHTML = media.description
viewNodes[68].textContent = ''
viewNodes[68].append(...mediaDetails(media))
viewNodes[66].textContent = ''
viewNodes[66].append(...mediaDetails(media))
}
export let alID // login icon
@ -454,3 +457,12 @@ export function initMenu () {
home.classList.add('noauth')
}
}
if (userBrowser === 'firefox') {
halfmoon.initStickyAlert({
content: 'Your browser will likely not support many video formats, containers and features. Please use Chromium!',
title: 'Bad Browser',
alertType: 'alert-danger',
fillType: ''
})
}

6
app/js/keys.js Normal file
View file

@ -0,0 +1,6 @@
export const keys = {
apiKey: '',
clientId: '',
scope: 'https://www.googleapis.com/auth/drive.readonly',
discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest']
}

View file

@ -7,6 +7,7 @@ import { alEntry } from './anilist.js'
import { settings } from './settings.js'
import { cardCreator, initMenu } from './interface.js'
import halfmoon from 'halfmoon'
import { GDHandleTorrent } from './webseed.js'
const playerControls = {}
for (const item of document.getElementsByClassName('ctrl')) {
@ -16,6 +17,9 @@ for (const item of document.getElementsByClassName('ctrl')) {
playerControls[item.dataset.name] = [playerControls[item.dataset.name], item]
}
}
window.addEventListener('beforeunload', () => {
client.destroy()
})
export const client = new WebTorrentPlayer({
WebTorrentOpts: {
maxConns: 127,
@ -23,7 +27,8 @@ export const client = new WebTorrentPlayer({
uploadLimit: settings.torrent7 * 1572864,
tracker: {
announce: settings.torrent10.split('\n')
}
},
destroyStore: true
},
controls: playerControls,
video: video,
@ -48,6 +53,7 @@ client.on('download-done', ({ file }) => {
fillType: ''
})
})
client.on('torrent', GDHandleTorrent)
client.on('watched', ({ filemedia }) => {
if (filemedia?.media?.episodes || filemedia?.media?.nextAiringEpisode?.episode) {
if (settings.other2 && (filemedia.media.episodes || filemedia.media.nextAiringEpisode?.episode > filemedia.episodeNumber)) {
@ -135,8 +141,6 @@ client.nowPlaying = { name: 'Miru' }
window.client = client
window.onbeforeunload = () => { return '' }
if (searchParams.get('file')) client.playTorrent(searchParams.get('file'))
queueMicrotask(initMenu)

View file

@ -39,7 +39,7 @@ export async function nyaaRss (media, episode, isOffline) {
const titles = [...new Set(Object.values(media.title).concat(media.synonyms).filter(name => name != null))].join(')|(').replace(/&/g, '%26')
const ep = (media.episodes !== 1 && ((media.status === 'FINISHED' && settings.torrent9) ? `"01-${media.episodes}"|"01~${media.episodes}"|"Batch"|"Complete"|"+${episode}+"|"+${episode}v"|"S01"` : `"+${episode}+"|"+${episode}v"`)) || ''
const excl = exclusions[userBrowser].join('|')
const quality = `"${settings.torrent1}"` || '"1080p"'
const quality = `${settings.torrent1}` || '"1080p"'
const trusted = settings.torrent3 === true ? 2 : 0
const url = new URL(`https://meowinjapanese.cf/?page=rss&c=1_2&f=${trusted}&s=seeders&o=desc&q=(${titles})${ep}${quality}-(${excl})`)
@ -101,8 +101,8 @@ export async function nyaaRss (media, episode, isOffline) {
export function getRSSurl () {
if (Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0]) {
return settings.torrent4 === 'Erai-raws' ? new URL(Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0].innerHTML + settings.torrent1 + '-magnet') : new URL(Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0].innerHTML + settings.torrent1)
return new URL(Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0].textContent + settings.torrent1)
} else {
return settings.torrent4 + settings.torrent1 // add custom RSS
return new URL(settings.torrent4 + settings.torrent1) // add custom RSS
}
}

View file

@ -1,7 +1,7 @@
/* global volume, player2, player3, player5, player6, player10, subtitle1, subtitle3, torrent1, torrent2, torrent3, torrent4, torrent5, torrent5label, torrent7, torrent8, torrent9, torrent10, other1, other2, setRes, settingsTab, regProtButton, clearRelCache */
/* global volume, player2, player3, player5, player6, player10, subtitle1, subtitle3, torrent1, torrent2, torrent3, torrent4, torrent5, torrent5label, torrent7, torrent8, torrent9, torrent10, other1, other2, other3, setRes, settingsTab, regProtButton, clearRelCache */
import { get, set, createStore } from 'idb-keyval'
export const settingsElements = [
volume, player2, player3, player5, player6, player10, subtitle1, subtitle3, torrent1, torrent2, torrent3, torrent4, torrent7, torrent8, torrent9, torrent10, other1, other2
volume, player2, player3, player5, player6, player10, subtitle1, subtitle3, torrent1, torrent2, torrent3, torrent4, torrent7, torrent8, torrent9, torrent10, other1, other2, other3
]
setRes.addEventListener('click', restoreDefaults)
settingsTab.addEventListener('click', applySettingsTimeout)
@ -39,10 +39,6 @@ async function saveSettings () {
}
}
if (Object.keys(settings).length !== settingsElements.length + 1) {
saveSettings()
}
let applyTimeout
function applySettingsTimeout () {
clearTimeout(applyTimeout)
@ -74,4 +70,6 @@ for (const setting of Object.entries(settings)) {
if (settingElement) settingElement.type === 'checkbox' ? settingElement.checked = setting[1] : settingElement.value = setting[1]
}
saveSettings()
other1.oninput = () => other1.checked && Notification.requestPermission().then(perm => { perm === 'denied' ? other1.checked = false : other1.checked = true })

View file

@ -1,23 +1,28 @@
import halfmoon from 'halfmoon'
halfmoon.showModal = id => {
const t = document.getElementById(id)
t && t.classList.add('show')
}
halfmoon.hideModal = id => {
const t = document.getElementById(id)
t && t.classList.remove('show')
}
export const searchParams = new URLSearchParams(location.href)
if (searchParams.get('access_token')) {
localStorage.setItem('ALtoken', searchParams.get('access_token'))
window.location = '/app/#home'
}
export const userBrowser = (() => {
if (window.chrome) {
return (navigator.userAgent.indexOf('Edg') !== -1) ? 'edge' : 'chromium'
}
return 'firefox'
})()
export function countdown (s) {
const d = Math.floor(s / (3600 * 24))
s -= d * 3600 * 24
@ -50,3 +55,67 @@ export function flattenObj (obj) {
}
export const DOMPARSER = new DOMParser().parseFromString.bind(new DOMParser())
export function concat (chunks, size) {
if (!size) {
size = 0
let i = chunks.length || chunks.byteLength || 0
while (i--) size += chunks[i].length
}
const b = new Uint8Array(size)
let offset = 0
for (let i = 0, l = chunks.length; i < l; i++) {
const chunk = chunks[i]
b.set(chunk, offset)
offset += chunk.byteLength || chunk.length
}
return b
}
export const sleep = t => new Promise(resolve => setTimeout(resolve, t))
export class Queue {
constructor () {
this.queue = []
this.destroyed = false
this.lastfn = null
}
add (obj) { // index, fn
if (this.destroyed) return
// most common case, requests are in order
// also push to the end of queue if there's an outstanding high range request [for example EOF metadata]
// this impacts backwards seeking performance a bit, but is needed for metadata
if (!this.queue.length || obj.index > this.queue[this.queue.length - 1].index || obj.index < this.queue[0].index - 10) {
this.queue.push(obj)
if (this.queue.length === 1) this._next()
} else {
// otherwise if one request failed its likely the oldest, or older one, so iterate backwards [forwards since queue is reversed]
for (let i = 0; i < this.queue.length; i++) {
if (this.queue[i].index > obj.index) {
this.queue.splice(i, 0, obj)
return
}
}
this.queue.push(obj)
console.warn('got bad')
}
}
async _next () {
const obj = this.queue[0]
await obj.fn()
this._remove(obj)
if (!this.destroyed && this.queue.length) this._next()
}
_remove (obj) {
if (!this.destroyed) this.queue.splice(this.queue.indexOf(obj), 1)
}
destroy () {
this.destroyed = true
this.queue = null
}
}

249
app/js/webseed.js Normal file
View file

@ -0,0 +1,249 @@
/* global gsignin */
import BitField from 'bitfield'
import sha1 from 'simple-sha1'
import Wire from 'bittorrent-protocol'
import { load, client, auth2 } from 'gapi'
import { keys } from './keys.js'
import { concat, sleep, Queue } from './util.js'
import halfmoon from 'halfmoon'
// i'll fucking give you 'thenable', shitass callbacks
export const token = new Promise(resolve => {
load('client:auth2', () => {
client.init(Object.assign({}, keys)).then(() => {
const auth = auth2.getAuthInstance()
auth.isSignedIn.listen(state => {
if (state === true) {
if (!auth.currentUser.get().getAuthResponse().scope.includes('https://www.googleapis.com/auth/drive.readonly')) {
halfmoon.initStickyAlert({
content: 'You didn\'t grant permission for Google Drive access, try again!',
title: 'Login Error',
alertType: 'alert-danger',
fillType: ''
})
return auth.signOut()
}
resolve(auth.currentUser.get().getAuthResponse().access_token)
gsignin.classList.add('d-none')
}
})
if (!auth.isSignedIn.get()) {
gsignin.onclick = auth.signIn
gsignin.classList.remove('d-none')
} else {
if (!auth.currentUser.get().getAuthResponse().scope.includes('https://www.googleapis.com/auth/drive.readonly')) {
halfmoon.initStickyAlert({
content: 'You didn\'t grant permission for Google Drive access, try again!',
title: 'Login Error',
alertType: 'alert-danger',
fillType: ''
})
return auth.signOut()
}
resolve(auth.currentUser.get().getAuthResponse().access_token)
}
})
})
})
export async function getFileNameID (name) {
return new Promise(resolve => {
token.then(() => {
client.drive.files.list({
pageSize: 1,
fields: 'files(id)',
includeItemsFromAllDrives: true,
supportsAllDrives: true,
corpora: 'allDrives',
q: `name = '${name}'`
}).then(res => {
console.log(res)
resolve(res?.result?.files?.length && res?.result?.files[0].id)
})
})
})
}
export async function GDHandleTorrent (torrent) {
await token
for await (const file of torrent.files) {
const gDriveID = await getFileNameID(file.name)
if (gDriveID) {
file.gDriveID = gDriveID
} else {
halfmoon.initStickyAlert({
content: `Couldn't find a GDrive file for ${file.name}! Can't create webseed!`,
title: 'GDrive Error!',
alertType: 'alert-secondary',
fillType: ''
})
return null // if a single entry doesnt exist, I can't create a reliable webseed
}
await sleep(1000)
}
torrent.addWebSeed(new WebConn(torrent))
}
class WebConn extends Wire {
constructor (torrent) {
super()
this.connId = torrent.infoHash + ' gdrive' // Unique id to deduplicate web seeds
this.webPeerId = sha1.sync(this.connId)
this._torrent = torrent
this.lastRequest = {}
this.sleep = null
this.queue = new Queue()
this._init()
}
_init () {
this.setKeepAlive(true)
this.once('handshake', (infoHash, peerId) => {
if (this.destroyed) return
this.handshake(infoHash, this.webPeerId)
const numPieces = this._torrent.pieces.length
const bitfield = new BitField(numPieces)
for (let i = 0; i <= numPieces; i++) {
bitfield.set(i, true)
}
this.bitfield(bitfield)
})
this.once('interested', () => {
this.unchoke()
})
this.on('request', (pieceIndex, offset, length, callback) => {
const request = () => this.queue.add({
fn: async () => await this.httpRequest(pieceIndex, offset, length, (err, data) => {
if (err || data?.length !== length) return request()
callback(err, data)
}),
index: pieceIndex
})
request()
})
}
async httpRequest (pieceIndex, offset, length, cb) {
const pieceOffset = pieceIndex * this._torrent.pieceLength
const rangeStart = pieceOffset + offset
const rangeEnd = rangeStart + length - 1
const files = this._torrent.files
let requests
if (files.length <= 1) {
requests = [{
url: `https://www.googleapis.com/drive/v3/files/${files[0].gDriveID}?supportsAllDrives=true&alt=media&key=${keys.apiKey}`,
start: rangeStart,
end: rangeEnd
}]
} else {
const requestedFiles = files.filter(file => file.offset <= rangeEnd && (file.offset + file.length) > rangeStart)
if (requestedFiles.length < 1) {
return cb(new Error('Could not find file corresponding to web seed range request'))
}
requests = requestedFiles.map(file => {
const fileEnd = file.offset + file.length - 1
const url = `https://www.googleapis.com/drive/v3/files/${file.gDriveID}?supportsAllDrives=true&alt=media&key=${keys.apiKey}`
return {
url,
fileOffsetInRange: Math.max(file.offset - rangeStart, 0),
start: Math.max(rangeStart - file.offset, 0),
end: Math.min(fileEnd, rangeEnd - file.offset)
}
})
}
let numRequestsSucceeded = 0
let hasError = false
let ret
if (requests.length > 1) {
ret = Buffer.alloc(length)
}
let { res, reader, endRange, setSize, ctrl } = this.lastRequest
for await (const request of requests) {
const { url, start, end } = request
function onResponse (res, data) {
if (!res || res.status < 200 || res.status >= 300) {
if (hasError) return
hasError = true
return cb(new Error(`Unexpected HTTP status code ${res?.status}`))
}
if (requests.length === 1) {
cb(null, data)
} else {
data.copy(ret, request.fileOffsetInRange)
if (++numRequestsSucceeded === requests.length) {
cb(null, ret)
}
}
}
if (endRange !== start - 1 || ctrl?.signal?.aborted) {
async function * read (reader) { // <3 Endless
let buffered = []
let bufferedBytes = 0
let done = false
let size = 512
const setSize = x => { size = x }
yield setSize
while (!done) {
const it = await reader.read()
done = it.done
if (done) {
yield concat(buffered, bufferedBytes)
return
} else {
bufferedBytes += it.value.byteLength
buffered.push(it.value)
while (bufferedBytes >= size) {
const b = concat(buffered)
bufferedBytes -= size
yield b.slice(0, size)
buffered = [b.slice(size, b.length)]
}
}
}
}
if (ctrl) ctrl.abort()
ctrl = new AbortController()
await this.sleep
try {
res = await fetch(url, {
headers: {
range: `bytes=${start}-`,
authorization: 'Bearer ' + await token
},
signal: ctrl.signal
})
} catch (e) {
this.sleep = sleep(500)
onResponse()
throw e
}
this.sleep = sleep(500)
reader = read(res.body.getReader(), 1)
setSize = (await reader.next()).value // lazy, but 1st yield is callback x)
}
endRange = end
setSize(end - start + 1)
onResponse(res, (await reader.next()).value || new Uint8Array())
}
this.lastRequest = { res, reader, endRange, setSize, ctrl }
}
destroy () {
this.lastRequest?.ctrl?.abort()
this.queue.destroy()
this.lastRequest?.ctrl?.abort()
super.destroy()
this._torrent = null
}
}

View file

@ -26,7 +26,7 @@ function addConnection (connection) {
peer = new window.Peer({ polite: false })
peer.pc.ontrack = evt => {
evt.receiver.playoutDelayHint = 3
evt.receiver.playoutDelayHint = 0.5
if (!video.srcObject) {
video.srcObject = evt.streams[0]
video.volume = 1

View file

@ -15,7 +15,7 @@
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
<meta name="viewport" content="width=device-width" />
<meta property="og:title" content="Miru">
<meta property="og:url" content="https://mirumoe.netlify.app/">
<meta property="og:url" content="https://miru.pages.dev/">
<meta property="og:description" content="Miru - Torrent streaming made simple!">
<meta property="og:type" content="video.other">
<meta property="og:image" content="logo.png">

View file

@ -7,7 +7,6 @@
"entertainment"
],
"lang": "en-US",
"orientation": "landscape",
"background_color": "#191c20",
"theme_color": "#191c20",
"scope": "/app/",
@ -28,14 +27,14 @@
}
},
"intent_filters": {
"scope_url_scheme": "http",
"scope_url_host": "localhost",
"scope_url_scheme": "https",
"scope_url_host": "miru.pages.dev",
"scope_url_path": "/app/"
},
"capture_links": "existing-client-navigate",
"url_handlers": [
{
"origin": "https://localhost:5500"
"origin": "https://miru.pages.dev"
}
],
"protocol_handlers": [

View file

@ -6,14 +6,14 @@ module.exports = {
externals: {
halfmoon: 'halfmoon',
anitomyscript: 'anitomyscript',
gapi: 'commonjs gapi'
gapi: 'globalThis.gapi'
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'app/js'),
sourceMapFilename: 'bundle.js.map'
},
mode: 'development',
mode: 'production',
devtool: 'source-map',
plugins: [
new webpack.ProvidePlugin({