mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-13 12:10:31 +00:00
first commit
This commit is contained in:
commit
b33cab060f
19 changed files with 12889 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/aniflix
|
||||
/compiled
|
||||
10899
api fetch/css.css
Normal file
10899
api fetch/css.css
Normal file
File diff suppressed because it is too large
Load diff
76
api fetch/css1.css
Normal file
76
api fetch/css1.css
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
::-webkit-scrollbar {
|
||||
display: none
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.bg-tp-dark {
|
||||
background: rgba(48, 48, 48, 0.6)
|
||||
}
|
||||
.bg-black{
|
||||
background: #222
|
||||
}
|
||||
|
||||
.cover-img {
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
width: 100%
|
||||
}
|
||||
.contain-img {
|
||||
object-fit: contain;
|
||||
width: 100%
|
||||
}
|
||||
.banner {
|
||||
height: 400px;
|
||||
opacity: 0.85
|
||||
}
|
||||
.mt-nc{
|
||||
margin-top: -150px
|
||||
}
|
||||
.view{
|
||||
position: fixed;
|
||||
z-index: 9;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0
|
||||
}
|
||||
.desc {
|
||||
max-height: 210px;
|
||||
overflow: scroll
|
||||
}
|
||||
|
||||
.ovf-y-scroll {
|
||||
overflow-y: scroll
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
.search {
|
||||
background: #444 !important;
|
||||
color: #ADB5BD !important;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.gallery .card {
|
||||
cursor: pointer;
|
||||
transition: transform .2s ease;
|
||||
}
|
||||
.gallery .card:hover {
|
||||
transform: scale(1.05)
|
||||
}
|
||||
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 540px);
|
||||
grid-auto-rows: 280px;
|
||||
justify-content: center;
|
||||
grid-gap: 2rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
280
api fetch/css2.css
Normal file
280
api fetch/css2.css
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
:root {
|
||||
font-size: 15px;
|
||||
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--nav-size: 5rem;
|
||||
--accent-color: #e5204c;
|
||||
--volume-level: 100%;
|
||||
--progress: 0%;
|
||||
--buffer: 0%;
|
||||
color: #b6b6b6;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cont {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: row
|
||||
}
|
||||
|
||||
/*NAVBAR*/
|
||||
|
||||
|
||||
nav {
|
||||
min-width: 5rem;
|
||||
height: 100vh;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
nav:hover ul {
|
||||
max-width: 13.4rem;
|
||||
transition: max-width .2s ease
|
||||
}
|
||||
|
||||
nav ul {
|
||||
overflow-x: hidden;
|
||||
max-width: var(--nav-size);
|
||||
background: #111;
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
transition: max-width .2s ease;
|
||||
}
|
||||
|
||||
nav li {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
nav li:last-child {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 5rem;
|
||||
text-decoration: none;
|
||||
filter: grayscale(100%) opacity(0.7);
|
||||
color: #b6b6b6;
|
||||
}
|
||||
|
||||
nav a:hover i {
|
||||
filter: drop-shadow(0 0 4px #888);
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
filter: grayscale(0%) opacity(1);
|
||||
color: #ececec;
|
||||
}
|
||||
|
||||
nav span {
|
||||
margin: 0 3rem 0 1rem;
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.nav i {
|
||||
font-size: 2rem !important;
|
||||
min-width: 2rem;
|
||||
margin: 0 1.5rem;
|
||||
}
|
||||
|
||||
nav img {
|
||||
width: 3rem;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
|
||||
/*PLAYER*/
|
||||
|
||||
|
||||
#player:target {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: none !important;
|
||||
display: flex
|
||||
}
|
||||
|
||||
#player {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
width: 25%;
|
||||
transition: width .2s ease;
|
||||
z-index: 10
|
||||
}
|
||||
|
||||
|
||||
#player video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#player:target a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#player a {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 10
|
||||
}
|
||||
|
||||
#player:target .controls {
|
||||
display: flex;
|
||||
font-family: Roboto, monospace
|
||||
}
|
||||
|
||||
#player .controls {
|
||||
display: none
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
align-self: flex-end;
|
||||
align-items: flex-end;
|
||||
user-select: none;
|
||||
min-width: 100%;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, .8), rgba(0, 0, 0, .4) 25%, rgba(0, 0, 0, .2) 50%, rgba(0, 0, 0, .1) 75%, transparent);
|
||||
opacity: 1;
|
||||
transition: .5s opacity ease
|
||||
}
|
||||
|
||||
.controls > *:hover {
|
||||
filter: drop-shadow(0 0 8px #000);
|
||||
}
|
||||
|
||||
.controls span {
|
||||
font-size: 1.7rem !important;
|
||||
color: #ececec;
|
||||
padding: 1rem;
|
||||
transition: all .2s ease;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.controls .ts {
|
||||
font-size: 1.3rem !important;
|
||||
white-space: nowrap;
|
||||
align-self: center;
|
||||
cursor: default;
|
||||
padding: 0 1rem;
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
#prog {
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.volume {
|
||||
display: flex;
|
||||
width: auto
|
||||
}
|
||||
|
||||
.volume:hover > input[type=range] {
|
||||
width: 5vw;
|
||||
display: inline-block;
|
||||
transition: all .1s ease;
|
||||
margin-right: 1rem
|
||||
}
|
||||
|
||||
.volume > input[type=range] {
|
||||
width: 0;
|
||||
transition: all .1s ease;
|
||||
margin-right: 0
|
||||
}
|
||||
|
||||
.controls input[type=range] {
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
.controls input[type=range]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.controls input#vol[type=range]::-webkit-slider-runnable-track {
|
||||
width: 50%;
|
||||
height: 3px;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(90deg, #e5204c var(--volume-level), rgba(255, 255, 255, .2) var(--volume-level))
|
||||
}
|
||||
|
||||
.controls input#prog[type=range]::-webkit-slider-runnable-track {
|
||||
width: 50%;
|
||||
height: 3px;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(90deg, #e5204c var(--progress), rgba(255, 255, 255, .4) var(--progress), rgba(255, 255, 255, .4) var(--buffer), rgba(255, 255, 255, .2) var(--buffer))
|
||||
}
|
||||
|
||||
.controls input[type=range]:hover::-webkit-slider-thumb {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
margin-top: -4px
|
||||
}
|
||||
|
||||
.controls input[type=range]::-webkit-slider-thumb {
|
||||
height: 0px;
|
||||
width: 0px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-color);
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
margin-top: 1px;
|
||||
transition: all .1s ease
|
||||
}
|
||||
|
||||
|
||||
/*BROWSE*/
|
||||
/*CONTAINER*/
|
||||
|
||||
#browse {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow-y: scroll
|
||||
}
|
||||
|
||||
#browse:target {
|
||||
display: flex
|
||||
}
|
||||
|
||||
/*PREVIEW*/
|
||||
|
||||
/*DETAILS*/
|
||||
|
||||
|
||||
/*SEARCH*/
|
||||
|
||||
|
||||
/*SECTIONS*/
|
||||
|
||||
|
||||
section {
|
||||
display: none
|
||||
}
|
||||
54
api fetch/id.html
Normal file
54
api fetch/id.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<script>
|
||||
var query = `
|
||||
query ($id: Int) { # Define which variables will be used in the query (id)
|
||||
Media (id: $id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Define our query variables and values that will be used in the query request
|
||||
var variables = {
|
||||
id: 16498
|
||||
};
|
||||
|
||||
// Define the config we'll need for our Api request
|
||||
var url = 'https://graphql.anilist.co',
|
||||
options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
variables: variables
|
||||
})
|
||||
};
|
||||
|
||||
// Make the HTTP Api request
|
||||
fetch(url, options).then(handleResponse)
|
||||
.then(handleData)
|
||||
.catch(handleError);
|
||||
|
||||
function handleResponse(response) {
|
||||
return response.json().then(function(json) {
|
||||
return response.ok ? json : Promise.reject(json);
|
||||
});
|
||||
}
|
||||
|
||||
function handleData(data) {
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
alert('Error, check console');
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
</script>
|
||||
85
api fetch/index.html
Normal file
85
api fetch/index.html
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<meta name="generator" content="">
|
||||
<title>API</title>
|
||||
|
||||
<link href="css.css" rel="stylesheet">
|
||||
<link href="css1.css" rel="stylesheet">
|
||||
<script src='js.js' defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="view bg-dark ovf-y-scroll" hidden>
|
||||
<div class="banner">
|
||||
<img class="cover-img">
|
||||
<button type="button" class="close" onclick="hideAnime()">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mt-nc pb-4">
|
||||
<img class="contain-img rounded-lg">
|
||||
</div>
|
||||
<div class="col-9 py-4">
|
||||
<h4 class="title">
|
||||
</h4>
|
||||
<p class="text-muted desc">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-black">
|
||||
<div class="container">
|
||||
<div class="row py-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-muted details">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<table class="table table-borderless table-hover rounded-lg overflow-hidden">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Size</th>
|
||||
<th scope="col">Seed</th>
|
||||
<th scope="col">Leech</th>
|
||||
<th scope="col">Downloads</th>
|
||||
<th scope="col">Play</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center pt-5">
|
||||
<div class="container">
|
||||
<form class="input-group mb-3" action="javascript:search()">
|
||||
<div class="input-group-prepend">
|
||||
<button class="btn btn-secondary" type="submit">O</button>
|
||||
</div>
|
||||
<input type="text" class="form-control search" placeholder="Search" id="search">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid h-100">
|
||||
<div class="gallery">
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
155
api fetch/index1.html
Normal file
155
api fetch/index1.html
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Titlename</title>
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css.css">
|
||||
<link rel="stylesheet" href="css1.css">
|
||||
<link rel="stylesheet" href="css2.css">
|
||||
<script src='js.js' defer></script>
|
||||
<script src="webtorrent.min.js"></script>
|
||||
<script src="playerHandler.js"></script>
|
||||
<script src="torrentHandler.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="cont">
|
||||
<nav id="nav">
|
||||
<ul class="nav">
|
||||
<li>
|
||||
<a>
|
||||
<img src="logofinal%20small.png">
|
||||
<span>Titlename</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#player">
|
||||
<i class="material-icons">play_arrow</i>
|
||||
<span>Play</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#releases">
|
||||
<i class="material-icons">schedule</i>
|
||||
<span>Releases</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#browse">
|
||||
<i class="material-icons">list</i>
|
||||
<span>Browse</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#settings">
|
||||
<i class="material-icons">settings</i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section id="player">
|
||||
<a href="#player"></a>
|
||||
<video id="video" src="hs.mkv" type="video/mp4">
|
||||
</video>
|
||||
<div class="controls">
|
||||
<span class="material-icons ctrl" title="Play/Pause [Space]" id="bpp">
|
||||
play_arrow
|
||||
</span>
|
||||
<span class="material-icons ctrl" title="Next [N]" id="bnext">
|
||||
skip_next
|
||||
</span>
|
||||
<div class="volume">
|
||||
<span class="material-icons ctrl" title="Mute [M]" id="bmute">
|
||||
volume_up
|
||||
</span>
|
||||
<input type="range" value="100" id="vol">
|
||||
</div>
|
||||
<span class="ts" id="elapsed">--:--</span>
|
||||
<input type="range" min="0" max="1000" value="0" id="prog">
|
||||
<span class="ts" id="remaining">--:--</span>
|
||||
<span class="material-icons ctrl" title="Popout Window [P]" id="bpip">
|
||||
picture_in_picture
|
||||
</span>
|
||||
<span class="material-icons ctrl" title="Theatre Mode [T]" id="btheatre">
|
||||
crop_16_9
|
||||
</span>
|
||||
<span class="material-icons ctrl" title="Fullscreen [F]" id="bfull">
|
||||
fullscreen
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<section id="browse">
|
||||
<div class="view bg-dark ovf-y-scroll" hidden>
|
||||
<div class="banner">
|
||||
<img class="cover-img">
|
||||
<button type="button" class="close" onclick="hideAnime()">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mt-nc pb-4">
|
||||
<img class="contain-img rounded-lg">
|
||||
</div>
|
||||
<div class="col-9 py-4">
|
||||
<h4 class="title">
|
||||
</h4>
|
||||
<p class="text-muted desc">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-black">
|
||||
<div class="container">
|
||||
<div class="row py-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-muted details">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<table class="table table-borderless table-hover rounded-lg overflow-hidden">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Size</th>
|
||||
<th scope="col">Seed</th>
|
||||
<th scope="col">Leech</th>
|
||||
<th scope="col">Downloads</th>
|
||||
<th scope="col">Play</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center pt-5">
|
||||
<div class="container">
|
||||
<form class="input-group mb-3" action="javascript:search()">
|
||||
<div class="input-group-prepend">
|
||||
<button class="btn btn-secondary" type="submit">O</button>
|
||||
</div>
|
||||
<input type="text" class="form-control search" placeholder="Search" id="search">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid h-100">
|
||||
<div class="gallery">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
230
api fetch/js.js
Normal file
230
api fetch/js.js
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
var query,
|
||||
variables = {
|
||||
type: "ANIME",
|
||||
page: 1,
|
||||
perPage: 50
|
||||
},
|
||||
request;
|
||||
|
||||
var url = 'https://graphql.anilist.co',
|
||||
options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
variables: variables
|
||||
})
|
||||
};
|
||||
|
||||
function search() {
|
||||
let search = document.querySelector("#search").value
|
||||
if (search == "") {
|
||||
alRequest()
|
||||
} else {
|
||||
alRequest(search)
|
||||
}
|
||||
}
|
||||
|
||||
function alRequest(a) {
|
||||
if (a == undefined) {
|
||||
variables.sort = "TRENDING_DESC"
|
||||
delete variables.search
|
||||
query = `
|
||||
query ($page: Int, $perPage: Int, $sort: [MediaSort], $type: MediaType) {
|
||||
Page (page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
total
|
||||
currentPage
|
||||
lastPage
|
||||
hasNextPage
|
||||
perPage
|
||||
}
|
||||
media(type: $type, sort: $sort) {
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
description(
|
||||
asHtml: true
|
||||
)
|
||||
season
|
||||
seasonYear
|
||||
format
|
||||
status
|
||||
episodes
|
||||
duration
|
||||
averageScore
|
||||
genres
|
||||
coverImage {
|
||||
large
|
||||
}
|
||||
bannerImage
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
} else {
|
||||
variables.search = a
|
||||
delete variables.sort
|
||||
query = `
|
||||
query ($page: Int, $perPage: Int, $search: String, $type: MediaType) {
|
||||
Page (page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
total
|
||||
currentPage
|
||||
lastPage
|
||||
hasNextPage
|
||||
perPage
|
||||
}
|
||||
media (type: $type, search: $search) {
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
description(
|
||||
asHtml: true
|
||||
)
|
||||
season
|
||||
seasonYear
|
||||
format
|
||||
status
|
||||
episodes
|
||||
duration
|
||||
averageScore
|
||||
genres
|
||||
coverImage {
|
||||
large
|
||||
}
|
||||
bannerImage
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
options.body = JSON.stringify({
|
||||
query: query,
|
||||
variables: variables
|
||||
})
|
||||
fetch(url, options).then((handleResponse))
|
||||
.then(handleData)
|
||||
.catch((error) => console.error(error));
|
||||
|
||||
function handleResponse(response) {
|
||||
return response.json().then(function (json) {
|
||||
return response.ok ? json : Promise.reject(json);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleData(data) {
|
||||
request = data
|
||||
console.log(request);
|
||||
let frag = document.createDocumentFragment(),
|
||||
hasBegun = true
|
||||
try {
|
||||
data.data.Page.media.forEach((media, index) => {
|
||||
let template = document.createElement("div")
|
||||
template.classList.add("card", "bg-dark")
|
||||
template.innerHTML = `
|
||||
<div class="row no-gutters h-100">
|
||||
<div class="col-4 h-100">
|
||||
<img src="${media.coverImage.large}" class="cover-img">
|
||||
<div class="card-img-overlay d-flex align-content-end flex-wrap p-0">
|
||||
<div class="bg-tp-dark d-flex flex-grow-1 px-3 py-2">
|
||||
${!!media.title.english ? media.title.english : media.title.romaji}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-8 h-100 card-grid">
|
||||
<div class="card-header px-3 pb-1">
|
||||
<h5 class="m-0 text-capitalize">${(!!media.season ? media.season.toLowerCase()+" ": "") + (!!media.seasonYear ? media.seasonYear : "")}</h5>
|
||||
<p class="card-text text-muted mb-0 text-capitalize"><small>${((!!media.format ? (media.format == "TV" ? media.format + " Show" : media.format) + " • " : "")+(!!media.episodes ? media.episodes + " Episodes • " : (!!media.duration ? media.duration + " Minutes • " : "" ))+(!!media.status ? media.status.toLowerCase() : ""))}</small></p>
|
||||
</div>
|
||||
<div class="card-body ovf-y-scroll px-3 py-2">
|
||||
<p class="card-text mb-0">${media.description}</p>
|
||||
</div>
|
||||
<div class="card-footer px-3 py-2">
|
||||
${media.genres.map(key => (`<span class="badge badge-pill badge-primary">${key}</span> `)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
template.onclick = function () {
|
||||
viewAnime(index)
|
||||
}
|
||||
frag.appendChild(template)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
if (hasBegun) {
|
||||
document.querySelector('.gallery').textContent = '';
|
||||
hasBegun = false;
|
||||
}
|
||||
document.querySelector('.gallery').appendChild(frag)
|
||||
}
|
||||
function hideAnime(){
|
||||
document.querySelector(".view").setAttribute("hidden", "")
|
||||
}
|
||||
|
||||
function viewAnime(a) {
|
||||
let media = request.data.Page.media[a]
|
||||
let details =["title.english","title.romaji","status","season","seasonYear","episodes","duration","format","averageScore"]
|
||||
document.querySelector(".view").removeAttribute("hidden")
|
||||
document.querySelector(".view .banner img").src = media.bannerImage
|
||||
document.querySelector(".view .contain-img").src = media.coverImage.large
|
||||
document.querySelector(".view .contain-img").src = media.coverImage.large
|
||||
document.querySelector(".view .title").textContent = !!media.title.english ? media.title.english : media.title.romaji
|
||||
document.querySelector(".view .desc").innerHTML = !!media.description ? media.description : ""
|
||||
tsearch(a,1)
|
||||
}
|
||||
const DOMPARSER = new DOMParser().parseFromString.bind(new DOMParser())
|
||||
|
||||
|
||||
function tsearch(a, b) {
|
||||
let name = request.data.Page.media[a].title.romaji
|
||||
if (b < 10) {
|
||||
b = "0" + b
|
||||
}
|
||||
let url = new URL("https://nyaa.si/?page=rss&c=1_2&f=2&s=seeders&o=desc&q=" + name + " \" " + b + " \"")
|
||||
let frag = document.createDocumentFragment(),
|
||||
hasBegun = true
|
||||
fetch(url).then((res) => {
|
||||
res.text().then((xmlTxt) => {
|
||||
try {
|
||||
let doc = DOMPARSER(xmlTxt, "text/xml")
|
||||
doc.querySelectorAll('item').forEach((item, index) => {
|
||||
let i = item.querySelector.bind(item),
|
||||
template = document.createElement("tr")
|
||||
template.innerHTML = `
|
||||
<th scope="row">${(index+1)}</th>
|
||||
<td>${i("title").textContent}</td>
|
||||
<td>${i("size").textContent}</td>
|
||||
<td>${i("seeders").textContent}</td>
|
||||
<td>${i("leechers").textContent}</td>
|
||||
<td>${i("downloads").textContent}</td>
|
||||
<td onclick="console.log('${i('link').textContent}')">Play</td>
|
||||
`
|
||||
frag.appendChild(template)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
if (hasBegun) {
|
||||
document.querySelector('tbody').textContent = '';
|
||||
hasBegun = false;
|
||||
}
|
||||
document.querySelector('tbody').appendChild(frag)
|
||||
})
|
||||
}).catch(() => console.error('Error in fetching the RSS feed'))
|
||||
}
|
||||
|
||||
|
||||
alRequest()
|
||||
BIN
api fetch/logofinal small.png
Normal file
BIN
api fetch/logofinal small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
297
api fetch/playerHandler.js
Normal file
297
api fetch/playerHandler.js
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
let controls,
|
||||
video,
|
||||
player,
|
||||
volume;
|
||||
|
||||
//create event listeners after page load
|
||||
|
||||
document.onreadystatechange = function () {
|
||||
if (document.readyState == "interactive") {
|
||||
controls = document.getElementsByClassName('ctrl');
|
||||
video = document.querySelector('#video');
|
||||
player = document.querySelector('#player');
|
||||
volume = document.querySelector('#vol');
|
||||
progress = document.querySelector('#prog');
|
||||
|
||||
volume.addEventListener("input", function () {
|
||||
updatevolume(null)
|
||||
});
|
||||
progress.addEventListener("input", setprogress);
|
||||
video.addEventListener("playing", playcheck);
|
||||
video.addEventListener("canplay", updateDisplay);
|
||||
video.addEventListener("loadedmetadata", setduration);
|
||||
video.addEventListener("click", bpp)
|
||||
immerse();
|
||||
|
||||
for (let i = 0; i < controls.length; i++) {
|
||||
controls[i].addEventListener("click", function () {
|
||||
let func = this.id;
|
||||
window[func]()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
//immerse timeout
|
||||
let immersetime;
|
||||
|
||||
function immerse() {
|
||||
document.onmousemove = resetTimer;
|
||||
document.onkeypress = resetTimer;
|
||||
function immerseplayer() {
|
||||
document.querySelector('#player').classList.add('immersed')
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
clearTimeout(immersetime);
|
||||
document.querySelector('#player').classList.remove('immersed')
|
||||
immersetime = setTimeout(immerseplayer, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
//set duration
|
||||
let duration;
|
||||
|
||||
function setduration() {
|
||||
duration = video.duration;
|
||||
}
|
||||
|
||||
//progress
|
||||
|
||||
function setprogress() {
|
||||
video.currentTime = progress.value / 1000 * duration;
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
function updateDisplay() {
|
||||
let progresspercent = (video.currentTime / duration * 100).toFixed(4),
|
||||
bufferpercent = (video.buffered.end(0) / duration * 100).toFixed(4),
|
||||
remaining = duration - video.currentTime
|
||||
document.documentElement.style.setProperty("--progress", progresspercent + "%");
|
||||
document.documentElement.style.setProperty("--buffer", bufferpercent + "%");
|
||||
document.querySelector("#elapsed").innerHTML = toTS(video.currentTime);
|
||||
document.querySelector("#remaining").innerHTML = toTS(remaining);
|
||||
progress.value = Math.floor(progresspercent * 10)
|
||||
}
|
||||
|
||||
function toTS(sec) {
|
||||
var hours = Math.floor(sec / 3600);
|
||||
var minutes = Math.floor((sec - (hours * 3600)) / 60);
|
||||
var seconds = Math.floor(sec - (hours * 3600) - (minutes * 60));
|
||||
if (minutes < 10) {
|
||||
minutes = "0" + minutes;
|
||||
}
|
||||
if (seconds < 10) {
|
||||
seconds = "0" + seconds;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return hours + ':' + minutes + ':' + seconds;
|
||||
} else {
|
||||
return minutes + ':' + seconds;
|
||||
}
|
||||
}
|
||||
|
||||
let islooped;
|
||||
|
||||
function playcheck() {
|
||||
if (!islooped && !video.paused) {
|
||||
islooped = true;
|
||||
updateDisplay();
|
||||
setTimeout(function () {
|
||||
islooped = false;
|
||||
playcheck();
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
|
||||
//play/pause button
|
||||
|
||||
function bpp() {
|
||||
let btnpp = document.querySelector('#bpp')
|
||||
if (video.paused) {
|
||||
btnpp.innerHTML = "pause";
|
||||
video.play();
|
||||
} else {
|
||||
btnpp.innerHTML = "play_arrow";
|
||||
video.pause();
|
||||
}
|
||||
}
|
||||
|
||||
function bnext() {
|
||||
console.log("todo")
|
||||
// TODO: get magnet link of next current next episode [using search]
|
||||
}
|
||||
|
||||
//volume shit
|
||||
|
||||
let oldlevel;
|
||||
|
||||
function bmute() {
|
||||
if (video.volume == 0) {
|
||||
updatevolume(oldlevel)
|
||||
} else {
|
||||
oldlevel = video.volume * 100
|
||||
updatevolume(0)
|
||||
}
|
||||
}
|
||||
|
||||
function updatevolume(a) {
|
||||
let btnm = document.querySelector("#bmute"),
|
||||
level;
|
||||
if (a == null) {
|
||||
level = volume.value;
|
||||
} else {
|
||||
level = a;
|
||||
volume.value = a;
|
||||
}
|
||||
document.documentElement.style.setProperty("--volume-level", level + "%");
|
||||
btnm.innerHTML = (level == 0) ? "volume_off" : "volume_up";
|
||||
video.volume = level / 100
|
||||
}
|
||||
|
||||
|
||||
//PiP
|
||||
|
||||
function bpip() {
|
||||
if (!document.pictureInPictureElement) {
|
||||
video.requestPictureInPicture();
|
||||
|
||||
} else {
|
||||
if (document.pictureInPictureElement) {
|
||||
document.exitPictureInPicture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//theathe mode
|
||||
|
||||
function btheatre() {
|
||||
let nav = document.querySelector('#nav');
|
||||
nav.classList.toggle('theatre');
|
||||
}
|
||||
|
||||
//fullscreen
|
||||
|
||||
function bfull() {
|
||||
let btnfull = document.querySelector('#bfull')
|
||||
if (!document.fullscreenElement) {
|
||||
player.requestFullscreen();
|
||||
btnfull.innerHTML = "fullscreen_exit"
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
btnfull.innerHTML = "fullscreen"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function seek(a) {
|
||||
video.currentTime += a;
|
||||
}
|
||||
|
||||
//bar related shit
|
||||
|
||||
function getPositionP(e) {
|
||||
let element = e.target.getBoundingClientRect();
|
||||
let x = (e.offsetX / element.width).toFixed(5);
|
||||
return {
|
||||
x
|
||||
};
|
||||
}
|
||||
|
||||
function printPosition(e) {
|
||||
let positionP = getPositionP(e);
|
||||
e.onmousemove = function () {
|
||||
document.querySelector('#vol').innerHTML = positionP.x
|
||||
}
|
||||
}
|
||||
|
||||
//keybinds
|
||||
|
||||
document.onkeydown = function (a) {
|
||||
switch (a.key) {
|
||||
case " ":
|
||||
bpp();
|
||||
break;
|
||||
case "n":
|
||||
bnext();
|
||||
break;
|
||||
case "m":
|
||||
bmute();
|
||||
break;
|
||||
case "p":
|
||||
bpip();
|
||||
break;
|
||||
case "t":
|
||||
btheatre();
|
||||
break;
|
||||
case "f":
|
||||
bfull();
|
||||
break;
|
||||
case "s":
|
||||
seek(89);
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
seek(-2);
|
||||
break;
|
||||
case "ArrowRight":
|
||||
seek(2);
|
||||
}
|
||||
}
|
||||
|
||||
//media session
|
||||
if ('mediaSession' in navigator) {
|
||||
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: 'Never Gonna Give You Up',
|
||||
artist: 'Rick Astley',
|
||||
album: 'Whenever You Need Somebody',
|
||||
artwork: [
|
||||
{
|
||||
src: 'https://dummyimage.com/96x96',
|
||||
sizes: '96x96',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: 'https://dummyimage.com/128x128',
|
||||
sizes: '128x128',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: 'https://dummyimage.com/192x192',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: 'https://dummyimage.com/256x256',
|
||||
sizes: '256x256',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: 'https://dummyimage.com/384x384',
|
||||
sizes: '384x384',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: 'https://dummyimage.com/512x512',
|
||||
sizes: '512x512',
|
||||
type: 'image/png'
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('play', function () {
|
||||
bpp();
|
||||
});
|
||||
navigator.mediaSession.setActionHandler('pause', function () {
|
||||
bpp();
|
||||
});
|
||||
navigator.mediaSession.setActionHandler('seekbackward', function () {
|
||||
seek(-2);
|
||||
});
|
||||
navigator.mediaSession.setActionHandler('seekforward', function () {
|
||||
seek(2);
|
||||
});
|
||||
navigator.mediaSession.setActionHandler('nexttrack', function () {
|
||||
bnext();
|
||||
});
|
||||
}
|
||||
77
api fetch/search.html
Normal file
77
api fetch/search.html
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<script>
|
||||
var query = `
|
||||
query ($page: Int, $perPage: Int, $search: String, $type: MediaType) {
|
||||
Page (page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
total
|
||||
currentPage
|
||||
lastPage
|
||||
hasNextPage
|
||||
perPage
|
||||
}
|
||||
media (type: $type, search: $search) {
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
description(
|
||||
asHtml: true
|
||||
)
|
||||
season
|
||||
seasonYear
|
||||
format
|
||||
status
|
||||
episodes
|
||||
duration
|
||||
averageScore
|
||||
genres
|
||||
coverImage {
|
||||
large
|
||||
}
|
||||
bannerImage
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
var variables = {
|
||||
search: "Fate/Zero",
|
||||
type: "ANIME",
|
||||
page: 1,
|
||||
perPage: 50
|
||||
};
|
||||
|
||||
var url = 'https://graphql.anilist.co',
|
||||
options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
variables: variables
|
||||
})
|
||||
};
|
||||
|
||||
fetch(url, options).then(handleResponse)
|
||||
.then(handleData)
|
||||
.catch(handleError);
|
||||
|
||||
function handleResponse(response) {
|
||||
return response.json().then(function(json) {
|
||||
return response.ok ? json : Promise.reject(json);
|
||||
});
|
||||
}
|
||||
|
||||
function handleData(data) {
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
</script>
|
||||
26
api fetch/torrentHandler.js
Normal file
26
api fetch/torrentHandler.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
var client = new WebTorrent()
|
||||
let lastTorrent;
|
||||
|
||||
function playTorrent(magnetlink, hasName) {
|
||||
|
||||
if (lastTorrent) {
|
||||
client.remove(magnetlink)
|
||||
}
|
||||
lastTorrent = magnetlink
|
||||
console.log(magnetlink)
|
||||
client.add(magnetlink, function (torrent) {
|
||||
console.log('Client is downloading:', torrent.infoHash)
|
||||
var file = torrent.files.find(function (file) {
|
||||
return file.name.endsWith('.mp4'||'.mkv')
|
||||
})
|
||||
file.renderTo('video', {
|
||||
autoplay: true,
|
||||
controls: false
|
||||
});
|
||||
})
|
||||
}
|
||||
client.on('error', function (err) {
|
||||
console.error('ERROR: ' + err.message)
|
||||
})
|
||||
//'magnet:?xt=urn:btih:W4YM35DEUBTUS6BY2YEBHD5KVWQANCOE&tr=http://nyaa.tracker.wf:7777/announce&tr=udp://tracker.coppersurfer.tk:6969/announce&tr=udp://tracker.internetwarriors.net:1337/announce&tr=udp://tracker.leechersparadise.org:6969/announce&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://open.stealth.si:80/announce&tr=udp://p4p.arenabg.com:1337/announce&tr=udp://mgtracker.org:6969/announce&tr=udp://tracker.tiny-vps.com:6969/announce&tr=udp://peerfect.org:6969/announce&tr=http://share.camoe.cn:8080/announce&tr=http://t.nyaatracker.com:80/announce&tr=https://open.kickasstracker.com:443/announce&ix=0'
|
||||
//'magnet:?xt=urn:btih:OWRWEI2ABTSOUYBG6MMBRPC3LT22HMGV&tr=http://nyaa.tracker.wf:7777/announce&tr=udp://tracker.coppersurfer.tk:6969/announce&tr=udp://tracker.internetwarriors.net:1337/announce&tr=udp://tracker.leechersparadise.org:6969/announce&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://open.stealth.si:80/announce&tr=udp://p4p.arenabg.com:1337/announce&tr=udp://mgtracker.org:6969/announce&tr=udp://tracker.tiny-vps.com:6969/announce&tr=udp://peerfect.org:6969/announce&tr=http://share.camoe.cn:8080/announce&tr=http://t.nyaatracker.com:80/announce&tr=https://open.kickasstracker.com:443/announce'
|
||||
20
api fetch/trending.html
Normal file
20
api fetch/trending.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>API request</title>
|
||||
<script src='js.js' defer></script>
|
||||
<link rel='stylesheet' href='css.css'>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Anime</h1>
|
||||
<input type="text" placeholder="Name" id="searchName"><button onclick="search()">Search</button>
|
||||
|
||||
<div id="results">
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
53
api fetch/webtorrent.chromeapp.js
Normal file
53
api fetch/webtorrent.chromeapp.js
Normal file
File diff suppressed because one or more lines are too long
6
api fetch/webtorrent.min.js
vendored
Normal file
6
api fetch/webtorrent.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
221
test/index - Copy.html
Normal file
221
test/index - Copy.html
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>browser server test</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/webtorrent/0.102.4/webtorrent.min.js"></script>
|
||||
<script src="range-parser.js"></script>
|
||||
<script>
|
||||
const client = new WebTorrent()
|
||||
const announceList = [
|
||||
['udp://tracker.openbittorrent.com:80'],
|
||||
['udp://tracker.internetwarriors.net:1337'],
|
||||
['udp://tracker.leechers-paradise.org:6969'],
|
||||
['udp://tracker.coppersurfer.tk:6969'],
|
||||
['udp://exodus.desync.com:6969'],
|
||||
['wss://tracker.webtorrent.io'],
|
||||
['wss://tracker.btorrent.xyz'],
|
||||
['wss://tracker.openwebtorrent.com'],
|
||||
['wss://tracker.fastcast.nz']
|
||||
]
|
||||
|
||||
WEBTORRENT_ANNOUNCE = announceList
|
||||
.map(function (arr) {
|
||||
return arr[0]
|
||||
})
|
||||
.filter(function (url) {
|
||||
return url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0
|
||||
})
|
||||
const sintel =
|
||||
'magnet:?xt=urn:btih:da225bd2c3ce14a4cf41fd0a32e0c9811b186812&dn=%5BHorribleSubs%5D%20Sword%20Art%20Online%20-%20Alicization%20-%20War%20of%20Underworld%20-%2018%20%5B1080p%5D.mkv&tr=http%3A%2F%2Fnyaa.tracker.wf%3A7777%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Fexodus.desync.com%3A6969%2Fannounce'
|
||||
|
||||
// const sintel =
|
||||
// 'magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F'
|
||||
|
||||
client.add(sintel, async function (torrent) {
|
||||
const server = await torrent.createServer()
|
||||
await server.listen()
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = a.innerText = `./webtorrent/${torrent.infoHash}/`
|
||||
a.innerText += ' (opens in a new tab - since you need to have this page open for webtorrent to work)'
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a)
|
||||
|
||||
const video = document.createElement('video')
|
||||
video.controls = true
|
||||
video.src = `./webtorrent/${torrent.infoHash}/${torrent.files[0].path}`
|
||||
document.body.appendChild(video)
|
||||
})
|
||||
|
||||
// Wish there where a easier way to get some of webtorrent's classes so i can patch stuff
|
||||
// const WebTorrent = require('webtorrent')
|
||||
// const { Torrent } = WebTorrent
|
||||
const dummyTorrent = client.add('06d67cc41f44fd57241551b6d95c2d1de38121ae')
|
||||
const torrentPrototype = Object.getPrototypeOf(dummyTorrent)
|
||||
client.remove('06d67cc41f44fd57241551b6d95c2d1de38121ae')
|
||||
|
||||
function getPageHTML(title, pageHtml) {
|
||||
return "<!DOCTYPE html><html><head><meta><title>" + title + "</title></head><body>" + pageHtml + "<body></html>";
|
||||
}
|
||||
|
||||
// From https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
|
||||
function encodeRFC5987(str) {
|
||||
return encodeURIComponent(str)
|
||||
// Note that although RFC3986 reserves "!", RFC5987 does not,
|
||||
// so we do not need to escape it
|
||||
.replace(/['()]/g, escape) // i.e., %27 %28 %29
|
||||
.replace(/\*/g, '%2A')
|
||||
// The following are not required for percent-encoding per RFC5987,
|
||||
// so we can allow for a little better readability over the wire: |`^
|
||||
.replace(/%(?:7C|60|5E)/g, unescape)
|
||||
}
|
||||
|
||||
torrentPrototype.createServer = function (requestListener) {
|
||||
if (this.destroyed) throw new Error('torrent is destroyed')
|
||||
|
||||
let registration = null
|
||||
const torrent = this
|
||||
|
||||
function serveIndexPage() {
|
||||
const listHtml = torrent.files.map((file, i) =>
|
||||
`<li><a x_download="${file.name}" href="${registration.scope}webtorrent/${torrent.infoHash}/${file.path}">${file.path}</a> (${file.length} bytes)</li>`
|
||||
).join('<br>')
|
||||
|
||||
const body = getPageHTML(
|
||||
`${torrent.name} - WebTorrent`,
|
||||
`<h1>${torrent.name}</h1><ol>${listHtml}</ol>`
|
||||
)
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/html'
|
||||
},
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
function serve404Page() {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'text/html'
|
||||
},
|
||||
body: getPageHTML('404 - Not Found', '<h1>404 - Not Found</h1>')
|
||||
}
|
||||
}
|
||||
|
||||
function serveFile(file, req) {
|
||||
const res = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'video/mp4',
|
||||
// Support range-requests
|
||||
'Accept-Ranges': 'bytes',
|
||||
// Set name of file (for "Save Page As..." dialog)
|
||||
'Content-Disposition': `inline; filename*=UTF-8''${encodeRFC5987(file.name)}`
|
||||
}
|
||||
}
|
||||
|
||||
// `rangeParser` returns an array of ranges, or an error code (number) if
|
||||
// there was an error parsing the range.
|
||||
let range = rangeParser(file.length, new Headers(req.headers).get('range') || '')
|
||||
|
||||
if (Array.isArray(range)) {
|
||||
res.status = 206 // indicates that range-request was understood
|
||||
|
||||
// no support for multi-range request, just use the first range
|
||||
range = range[0]
|
||||
|
||||
res.headers['Content-Range'] = `bytes ${range.start}-${range.end}/${file.length}`
|
||||
res.headers['Content-Length'] = `${range.end - range.start + 1}`
|
||||
} else {
|
||||
range = null
|
||||
res.headers['Content-Length'] = file.length
|
||||
}
|
||||
|
||||
if (req.method === 'HEAD') res.body = ''
|
||||
else res.stream = file.createReadStream(range)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', evt => {
|
||||
const root = new URL(registration.scope).pathname
|
||||
const url = new URL(evt.data.url)
|
||||
const pathname = url.pathname.split(`webtorrent/${torrent.infoHash}/`)[1]
|
||||
console.log(pathname)
|
||||
const respond = msg => evt.ports[0].postMessage(msg)
|
||||
|
||||
if (pathname === '') {
|
||||
return respond(serveIndexPage())
|
||||
}
|
||||
|
||||
const file = torrent.files[0]
|
||||
const res = serveFile(file, evt.data)
|
||||
if (res.stream) {
|
||||
const stream = res.stream
|
||||
delete res.stream
|
||||
|
||||
stream.once('end', () => {
|
||||
respond(null) // Signal end event
|
||||
evt.ports[0].onmessage = null
|
||||
})
|
||||
|
||||
evt.ports[0].onmessage = evt => {
|
||||
const chunk = stream.read()
|
||||
if (chunk === null) {
|
||||
stream.once('readable', () => {
|
||||
const chunk = stream.read()
|
||||
respond(new Uint8Array(chunk))
|
||||
})
|
||||
} else {
|
||||
respond(new Uint8Array(chunk))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
respond(res)
|
||||
})
|
||||
|
||||
const res = {
|
||||
listen(port) {
|
||||
const scope = `./`
|
||||
res.scope = scope
|
||||
return navigator.serviceWorker.getRegistration(scope).then(swReg => {
|
||||
return swReg || navigator.serviceWorker.register('sw.js', {
|
||||
scope
|
||||
})
|
||||
}).then(swReg => {
|
||||
registration = swReg
|
||||
res.scope = registration.scope
|
||||
res.registration = registration
|
||||
let swRegTmp = swReg.installing || swReg.waiting
|
||||
|
||||
if (swReg.active)
|
||||
return
|
||||
|
||||
return new Promise(rs => {
|
||||
swRegTmp.onstatechange = () => {
|
||||
if (swRegTmp.state === 'activated') rs()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
close() {
|
||||
registration && registration.unregister()
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
186
test/index.html
Normal file
186
test/index.html
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>browser server test</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/webtorrent/0.102.4/webtorrent.min.js"></script>
|
||||
<script src="range-parser.js"></script>
|
||||
<script>
|
||||
const client = new WebTorrent()
|
||||
const sintel = 'magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F'
|
||||
|
||||
client.add(sintel, async function(torrent) {
|
||||
const server = await torrent.createServer()
|
||||
await server.listen()
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = a.innerText = `./webtorrent/${torrent.infoHash}/`
|
||||
a.innerText += ' (opens in a new tab - since you need to have this page open for webtorrent to work)'
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a)
|
||||
|
||||
const video = document.createElement('video')
|
||||
video.controls = true
|
||||
video.src = `./webtorrent/${torrent.infoHash}/${torrent.files[5].path}`
|
||||
document.body.appendChild(video)
|
||||
})
|
||||
|
||||
// Wish there where a easier way to get some of webtorrent's classes so i can patch stuff
|
||||
// const WebTorrent = require('webtorrent')
|
||||
// const { Torrent } = WebTorrent
|
||||
const dummyTorrent = client.add('06d67cc41f44fd57241551b6d95c2d1de38121ae')
|
||||
const torrentPrototype = Object.getPrototypeOf(dummyTorrent)
|
||||
client.remove('06d67cc41f44fd57241551b6d95c2d1de38121ae')
|
||||
|
||||
function getPageHTML (title, pageHtml) {
|
||||
return "<!DOCTYPE html><html><head><meta><title>" + title + "</title></head><body>" + pageHtml + "<body></html>";
|
||||
}
|
||||
|
||||
// From https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
|
||||
function encodeRFC5987 (str) {
|
||||
return encodeURIComponent(str)
|
||||
// Note that although RFC3986 reserves "!", RFC5987 does not,
|
||||
// so we do not need to escape it
|
||||
.replace(/['()]/g, escape) // i.e., %27 %28 %29
|
||||
.replace(/\*/g, '%2A')
|
||||
// The following are not required for percent-encoding per RFC5987,
|
||||
// so we can allow for a little better readability over the wire: |`^
|
||||
.replace(/%(?:7C|60|5E)/g, unescape)
|
||||
}
|
||||
|
||||
torrentPrototype.createServer = function(requestListener) {
|
||||
if (this.destroyed) throw new Error('torrent is destroyed')
|
||||
|
||||
let registration = null
|
||||
const torrent = this
|
||||
|
||||
function serveIndexPage () {
|
||||
const listHtml = torrent.files.map((file, i) => `<li><a x_download="${file.name}" href="${registration.scope}webtorrent/${torrent.infoHash}/${file.path}">${file.path}</a> (${file.length} bytes)</li>`).join('<br>')
|
||||
|
||||
const body = getPageHTML(
|
||||
`${torrent.name} - WebTorrent`,
|
||||
`<h1>${torrent.name}</h1><ol>${listHtml}</ol>`
|
||||
)
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: {'Content-Type': 'text/html'},
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
function serve404Page () {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {'Content-Type': 'text/html'},
|
||||
body: getPageHTML('404 - Not Found', '<h1>404 - Not Found</h1>')
|
||||
}
|
||||
}
|
||||
|
||||
function serveFile (file, req) {
|
||||
const res = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': file._getMimeType(),
|
||||
// Support range-requests
|
||||
'Accept-Ranges': 'bytes',
|
||||
// Set name of file (for "Save Page As..." dialog)
|
||||
'Content-Disposition': `inline; filename*=UTF-8''${encodeRFC5987(file.name)}`
|
||||
}
|
||||
}
|
||||
|
||||
// `rangeParser` returns an array of ranges, or an error code (number) if
|
||||
// there was an error parsing the range.
|
||||
let range = rangeParser(file.length, new Headers(req.headers).get('range') || '')
|
||||
|
||||
if (Array.isArray(range)) {
|
||||
res.status = 206 // indicates that range-request was understood
|
||||
|
||||
// no support for multi-range request, just use the first range
|
||||
range = range[0]
|
||||
|
||||
res.headers['Content-Range'] = `bytes ${range.start}-${range.end}/${file.length}`
|
||||
res.headers['Content-Length'] = `${range.end - range.start + 1}`
|
||||
} else {
|
||||
range = null
|
||||
res.headers['Content-Length'] = file.length
|
||||
}
|
||||
|
||||
if (req.method === 'HEAD') res.body = ''
|
||||
else res.stream = file.createReadStream(range)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', evt => {
|
||||
const root = new URL(registration.scope).pathname
|
||||
const url = new URL(evt.data.url)
|
||||
const pathname = url.pathname.split(`webtorrent/${torrent.infoHash}/`)[1]
|
||||
const respond = msg => evt.ports[0].postMessage(msg)
|
||||
|
||||
if (pathname === '') {
|
||||
return respond(serveIndexPage())
|
||||
}
|
||||
|
||||
const file = torrent.files.find(f => f.path === pathname)
|
||||
const res = serveFile(file, evt.data)
|
||||
if (res.stream) {
|
||||
const stream = res.stream
|
||||
delete res.stream
|
||||
|
||||
stream.once('end', () => {
|
||||
respond(null) // Signal end event
|
||||
evt.ports[0].onmessage = null
|
||||
})
|
||||
|
||||
evt.ports[0].onmessage = evt => {
|
||||
const chunk = stream.read()
|
||||
if (chunk === null) {
|
||||
stream.once('readable', () => {
|
||||
const chunk = stream.read()
|
||||
respond(new Uint8Array(chunk))
|
||||
})
|
||||
} else {
|
||||
respond(new Uint8Array(chunk))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
respond(res)
|
||||
})
|
||||
|
||||
const res = {
|
||||
listen(port) {
|
||||
const scope = `./`
|
||||
res.scope = scope
|
||||
return navigator.serviceWorker.getRegistration(scope).then(swReg => {
|
||||
return swReg || navigator.serviceWorker.register('sw.js', { scope })
|
||||
}).then(swReg => {
|
||||
registration = swReg
|
||||
res.scope = registration.scope
|
||||
res.registration = registration
|
||||
let swRegTmp = swReg.installing || swReg.waiting
|
||||
|
||||
if (swReg.active)
|
||||
return
|
||||
|
||||
return new Promise(rs => {
|
||||
swRegTmp.onstatechange = () => {
|
||||
if (swRegTmp.state === 'activated') rs()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
close() {
|
||||
registration && registration.unregister()
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
162
test/range-parser.js
Normal file
162
test/range-parser.js
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/*!
|
||||
* range-parser
|
||||
* Copyright(c) 2012-2014 TJ Holowaychuk
|
||||
* Copyright(c) 2015-2016 Douglas Christopher Wilson
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
* @public
|
||||
*/
|
||||
|
||||
// module.exports = rangeParser
|
||||
|
||||
/**
|
||||
* Parse "Range" header `str` relative to the given file `size`.
|
||||
*
|
||||
* @param {Number} size
|
||||
* @param {String} str
|
||||
* @param {Object} [options]
|
||||
* @return {Array}
|
||||
* @public
|
||||
*/
|
||||
|
||||
function rangeParser (size, str, options) {
|
||||
if (typeof str !== 'string') {
|
||||
throw new TypeError('argument str must be a string')
|
||||
}
|
||||
|
||||
var index = str.indexOf('=')
|
||||
|
||||
if (index === -1) {
|
||||
return -2
|
||||
}
|
||||
|
||||
// split the range string
|
||||
var arr = str.slice(index + 1).split(',')
|
||||
var ranges = []
|
||||
|
||||
// add ranges type
|
||||
ranges.type = str.slice(0, index)
|
||||
|
||||
// parse all ranges
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
var range = arr[i].split('-')
|
||||
var start = parseInt(range[0], 10)
|
||||
var end = parseInt(range[1], 10)
|
||||
|
||||
// -nnn
|
||||
if (isNaN(start)) {
|
||||
start = size - end
|
||||
end = size - 1
|
||||
// nnn-
|
||||
} else if (isNaN(end)) {
|
||||
end = size - 1
|
||||
}
|
||||
|
||||
// limit last-byte-pos to current length
|
||||
if (end > size - 1) {
|
||||
end = size - 1
|
||||
}
|
||||
|
||||
// invalid or unsatisifiable
|
||||
if (isNaN(start) || isNaN(end) || start > end || start < 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
// add range
|
||||
ranges.push({
|
||||
start: start,
|
||||
end: end
|
||||
})
|
||||
}
|
||||
|
||||
if (ranges.length < 1) {
|
||||
// unsatisifiable
|
||||
return -1
|
||||
}
|
||||
|
||||
return options && options.combine
|
||||
? combineRanges(ranges)
|
||||
: ranges
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine overlapping & adjacent ranges.
|
||||
* @private
|
||||
*/
|
||||
|
||||
function combineRanges (ranges) {
|
||||
var ordered = ranges.map(mapWithIndex).sort(sortByRangeStart)
|
||||
|
||||
for (var j = 0, i = 1; i < ordered.length; i++) {
|
||||
var range = ordered[i]
|
||||
var current = ordered[j]
|
||||
|
||||
if (range.start > current.end + 1) {
|
||||
// next range
|
||||
ordered[++j] = range
|
||||
} else if (range.end > current.end) {
|
||||
// extend range
|
||||
current.end = range.end
|
||||
current.index = Math.min(current.index, range.index)
|
||||
}
|
||||
}
|
||||
|
||||
// trim ordered array
|
||||
ordered.length = j + 1
|
||||
|
||||
// generate combined range
|
||||
var combined = ordered.sort(sortByRangeIndex).map(mapWithoutIndex)
|
||||
|
||||
// copy ranges type
|
||||
combined.type = ranges.type
|
||||
|
||||
return combined
|
||||
}
|
||||
|
||||
/**
|
||||
* Map function to add index value to ranges.
|
||||
* @private
|
||||
*/
|
||||
|
||||
function mapWithIndex (range, index) {
|
||||
return {
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
index: index
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map function to remove index value from ranges.
|
||||
* @private
|
||||
*/
|
||||
|
||||
function mapWithoutIndex (range) {
|
||||
return {
|
||||
start: range.start,
|
||||
end: range.end
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort function to sort ranges by index.
|
||||
* @private
|
||||
*/
|
||||
|
||||
function sortByRangeIndex (a, b) {
|
||||
return a.index - b.index
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort function to sort ranges by start position.
|
||||
* @private
|
||||
*/
|
||||
|
||||
function sortByRangeStart (a, b) {
|
||||
return a.start - b.start
|
||||
}
|
||||
60
test/sw.js
Normal file
60
test/sw.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// Activate event
|
||||
// Be sure to call self.clients.claim()
|
||||
self.addEventListener('activate', evt => {
|
||||
// `claim()` sets this worker as the active worker for all clients that
|
||||
// match the workers scope and triggers an `oncontrollerchange` event for
|
||||
// the clients.
|
||||
return self.clients.claim()
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', evt => {
|
||||
const { request } = evt
|
||||
const { url, method } = request
|
||||
const headers = [...request.headers]
|
||||
if (!url.includes(self.registration.scope + 'webtorrent/')) return null
|
||||
|
||||
function getConsumer(clients) {
|
||||
return new Promise((rs, rj) => {
|
||||
// Use race condition for whoever controls the response stream
|
||||
for (const client of clients) {
|
||||
const mc = new MessageChannel()
|
||||
mc.port1.onmessage = evt => rs([evt.data, mc])
|
||||
client.postMessage({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
scope: self.registration.scope
|
||||
}, [mc.port2])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
evt.respondWith(
|
||||
clients.matchAll({ type: 'window', includeUncontrolled: true })
|
||||
.then(getConsumer)
|
||||
.then(([data, consumer]) => {
|
||||
const readable = 'body' in data ? data.body : new ReadableStream({
|
||||
pull(controller) {
|
||||
console.log('requesting data')
|
||||
return new Promise(rs => {
|
||||
consumer.port1.onmessage = evt => {
|
||||
evt.data
|
||||
? controller.enqueue(evt.data) // evt.data is Uint8Array
|
||||
: controller.close() // evt.data is null, means the stream ended
|
||||
rs()
|
||||
}
|
||||
consumer.port1.postMessage(true) // send a pull request
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
// This event is never executed
|
||||
console.log('request aborted')
|
||||
consumer.port1.postMessage(false) // send a cancel request
|
||||
}
|
||||
})
|
||||
|
||||
return new Response(readable, data)
|
||||
})
|
||||
.catch(console.error)
|
||||
)
|
||||
})
|
||||
Loading…
Reference in a new issue