initial commit

This commit is contained in:
Cloudburst 2022-08-26 15:46:21 +02:00
commit e2b460386e
40 changed files with 37733 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules/
.cache/
public

0
README.md Normal file
View file

1
gatsby-browser.js Normal file
View file

@ -0,0 +1 @@
require("prism-themes/themes/prism-dracula.min.css")

39
gatsby-config.js Normal file
View file

@ -0,0 +1,39 @@
module.exports = {
siteMetadata: require("./metadata"),
plugins: [
"gatsby-plugin-postcss",
'gatsby-plugin-preact',
{
resolve: 'gatsby-plugin-manifest',
options: require("./manifest")
},
{
resolve: 'gatsby-source-filesystem',
options: {
"name": "pages",
"path": "./src/pages/docs/",
},
__key: "pages"
},
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
{
resolve: `gatsby-remark-prismjs`,
options: {
showLineNumbers: false,
}
}
]
}
},
{
resolve: 'gatsby-plugin-html-attributes',
options: {
lang: 'en',
'data-theme': 'dark'
}
},
]
};

30
gatsby-node.js Normal file
View file

@ -0,0 +1,30 @@
const path = require(`path`)
const { node2slug } = require(`./src/utils.js`)
const template = path.resolve(`./src/templates/default.jsx`)
// Rest of createPages API...
exports.createPages = async ({ graphql, actions }) => {
const {data} = await graphql(`
{
allMarkdownRemark {
nodes {
id
frontmatter {
title
}
fileAbsolutePath
}
}
}
`)
data.allMarkdownRemark.nodes.forEach(node => {
actions.createPage({
path: node2slug(node),
component: `${template}?__contentFilePath=${node.fileAbsolutePath}`,
context: {
id: node.id,
},
})
})
}

12
manifest.js Normal file
View file

@ -0,0 +1,12 @@
const {title} = require("./metadata")
module.exports = {
name: title,
short_name: title,
start_url: '/',
background_color: '#282A36',
theme_color: '#44475A',
display: 'standalone',
lang: 'en',
icon: 'src/media/icon.svg',
}

4
metadata.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = {
title: `Recloudstream`,
siteUrl: `https://www.yourdomain.tld`
}

36039
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

40
package.json Normal file
View file

@ -0,0 +1,40 @@
{
"name": "recloudstream",
"version": "1.0.0",
"private": true,
"description": "recloudstream",
"author": "Cloudburst",
"keywords": [
"gatsby"
],
"scripts": {
"develop": "gatsby develop",
"start": "gatsby develop",
"build": "gatsby build",
"serve": "gatsby serve",
"clean": "gatsby clean"
},
"dependencies": {
"daisyui": "^2.24.0",
"gatsby": "^4.21.1",
"gatsby-plugin-html-attributes": "^1.0.5",
"gatsby-plugin-manifest": "^4.21.0",
"gatsby-plugin-postcss": "^5.21.0",
"gatsby-plugin-preact": "^6.21.0",
"gatsby-remark-prismjs": "^6.21.0",
"gatsby-source-filesystem": "^4.21.1",
"gatsby-transformer-remark": "^5.21.0",
"postcss": "^8.4.16",
"preact": "^10.10.6",
"prism-themes": "^1.9.0",
"prismjs": "^1.29.0",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-icons": "^4.4.0",
"rehype-react": "^7.1.1",
"tailwindcss": "^3.1.8"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.4"
}
}

4
postcss.config.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = {
plugins: [require('tailwindcss'), require('autoprefixer')],
};

View file

@ -0,0 +1,20 @@
import React from "react";
import { Link } from "gatsby";
import { node2slug } from "../../utils";
const DocsCard = ({ md }) => {
return <article className="card card bg-base-200 shadow-xl w-full mx-10 md:w-2/3 mb-5">
<div className="card-body">
<h2 className="card-title">{md.frontmatter.title}</h2>
<p>
{md.excerpt}
</p>
<div className="card-actions justify-end">
<Link to={node2slug(md)} className="btn btn-primary">View</Link>
</div>
</div>
</article>
}
export default DocsCard

View file

@ -0,0 +1,54 @@
import React, { useState, useEffect, useRef } from "react";
const RepoCard = ({ url, isFirst }) => {
const [data, setData] = useState(null)
const firstButton = useRef(null)
useEffect(() => {
if (!isFirst) return;
console.log({firstButton})
firstButton.current?.focus()
}, [firstButton])
useEffect(() => {
fetch(url)
.then(r => r.json())
.then(dat => {
setData(dat)
})
}, [setData])
if (data == null) {
return <progress className="progress progress-primary w-full mx-10 md:w-2/3 mb-5"></progress>
}
return <article className="card card bg-base-200 shadow-xl w-full mx-10 md:w-2/3 mb-5">
<div className="card-body">
<h2 className="card-title">{data.name || "Unnamed"}</h2>
<p>
{data.description || "No description provided"}
</p>
<div className="card-actions justify-end">
<div className="btn-group">
<button ref={firstButton} className="btn btn-primary" onClick={() => {
window.open(`cloudstreamrepo://${url.replace(/^https?:\/\//, "")}`)
}}>Install</button>
<button className="btn" onClick={() => {
if (navigator.clipboard) {
navigator.clipboard.writeText(url);
} else {
var tempInput = document.createElement("input");
tempInput.value = url;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand("copy");
document.body.removeChild(tempInput);
}
}}>Copy URL</button>
</div>
</div>
</div>
</article>
}
export default RepoCard

35
src/components/drawer.jsx Normal file
View file

@ -0,0 +1,35 @@
import { graphql, StaticQuery } from "gatsby"
import React from "react"
import { Link } from "gatsby"
import { node2slug } from "../utils"
const Drawer = () => (
<ul className="menu p-4 overflow-y-auto w-80 bg-base-100 text-base-content">
<li><Link to="/">Home</Link></li>
<li><Link to="/repos">Repositories</Link></li>
<div className="divider">Docs</div>
<li><Link to="/docs">Documentation</Link></li>
<li><a href="/dokka/">Dokka</a></li>
<StaticQuery
query={graphql`
query {
allMarkdownRemark {
nodes {
frontmatter {
title
}
fileAbsolutePath
}
}
}
`}
render={data => (
<>{data.allMarkdownRemark.nodes.map(it => {
return <li><Link to={node2slug(it)}>{it.frontmatter.title}</Link></li>
})}</>
)}
/>
</ul>)
export default Drawer

14
src/components/hero.jsx Normal file
View file

@ -0,0 +1,14 @@
import React from "react"
const Hero = ({bg, children}) => (
<div className="hero min-h-screen" style={{backgroundImage: `url(${bg})`}}>
<div className="hero-overlay bg-opacity-60" />
<div className="hero-content text-center text-neutral-content">
<div className="max-w-md">
{children}
</div>
</div>
</div>
)
export default Hero

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

24
src/components/layout.jsx Normal file
View file

@ -0,0 +1,24 @@
import React from "react"
import Navbar from "./navbar"
import Drawer from "./drawer"
import './layout.css'
const Layout = ({ children }) => (
<div className="drawer bg-base">
<input id="drawer" type="checkbox" className="drawer-toggle" />
<div className="drawer-content items-center flex flex-none flex-col">
<Navbar />
<div className="pb-10 w-full text-base-content">
{children}
</div>
</div>
<div className="drawer-side">
<label for="drawer" className="drawer-overlay backdrop-blur-sm"></label>
<Drawer />
</div>
</div>
)
export default Layout

41
src/components/navbar.jsx Normal file
View file

@ -0,0 +1,41 @@
import React from "react"
import {FaDiscord, FaGithub, FaBars} from "react-icons/fa"
import {SiMatrix} from "react-icons/si"
import Button from "./navbar/button"
import logo from "../media/icon.svg"
import { Link } from "gatsby"
const Navbar = () => (
<div className="sticky top-0 z-30 flex h-16 w-full justify-center bg-opacity-90 backdrop-blur text-primary-content">
<nav className="navbar w-full">
<div className="navbar-start flex flex-0 md:gap-1 lg:gap-2">
<span className="tooltip tooltip-bottom before:text-xs before:content-[attr(data-tip)]" data-tip="Menu">
<label for="drawer" className="btn btn-square btn-ghost drawer-button text-xl text-base-content">
<FaBars />
</label>
</span>
</div>
<div className="flex navbar-center items-center gap-2">
<Link className="flex flex-0 btn btn-ghost gap-4 text-base-content" to="/">
<img src={logo} alt="Logo" className="object-contain h-12 w-12" />
<span className="hidden md:inline">Cloudstream</span>
</Link>
</div>
<div className="flex flex-0 navbar-end text-base-content">
<Button name="Discord" url="https://discord.gg/5Hus6fM">
<FaDiscord />
</Button>
<Button name="Matrix" url="https://matrix.to/#/#lagrapps:matrix.org">
<SiMatrix />
</Button>
<Button name="GitHub" url="https://github.com/recloudstream/">
<FaGithub />
</Button>
</div>
</nav>
</div>
)
export default Navbar

View file

@ -0,0 +1,13 @@
import React from "react"
const Button = ({url, children, name}) => (
<span className="tooltip tooltip-bottom before:text-xs before:content-[attr(data-tip)]" data-tip={name}>
<div className="flex-none items-center">
<a className="btn btn-ghost drawer-button btn-square text-xl" href={url || "#!"}>
{children}
</a>
</div>
</span>
)
export default Button

54
src/media/icon.svg Executable file
View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="vector"
width="270"
height="270"
viewBox="0 0 270.00001 269.99999"
version="1.1"
sodipodi:docname="ic_launcher_foreground.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs116" />
<sodipodi:namedview
id="namedview114"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.97453704"
inkscape:cx="-41.045131"
inkscape:cy="20.522565"
inkscape:window-width="952"
inkscape:window-height="1014"
inkscape:window-x="5"
inkscape:window-y="5"
inkscape:window-maximized="1"
inkscape:current-layer="vector" />
<g
id="g_0"
transform="translate(-6.31,-17.168647)">
<path
fill="#2309db"
d="m 245.05,148.63 c -2.801,-0.003 -5.587,0.422 -8.26,1.26 -1.639,-8.526 -6.092,-16.26 -12.643,-21.959 -6.55,-5.698 -14.826,-9.038 -23.497,-9.481 -4.737,-13.019 -13.862,-23.992 -25.799,-31.023 -11.937,-7.031 -25.958,-9.692 -39.641,-7.522 -13.683,2.169 -26.193,9.036 -35.37,19.415 -9.969,-3.375 -20.789,-3.296 -30.707,0.225 -9.918,3.52 -18.368,10.281 -23.978,19.185 -5.61,8.904 -8.061,19.444 -6.955,29.91 h -0.26 c -7.325,0 -14.358,2.913 -19.537,8.093 -5.18,5.179 -8.093,12.212 -8.093,19.537 0,7.325 2.913,14.358 8.093,19.537 5.179,5.18 12.212,8.093 19.537,8.093 h 207.11 c 7.325,0 14.358,-2.913 19.537,-8.093 5.18,-5.179 8.093,-12.212 8.093,-19.537 0,-7.325 -2.913,-14.358 -8.093,-19.537 -5.179,-5.18 -12.212,-8.093 -19.537,-8.093 z"
stroke-width="1"
id="path106" />
<path
fill="#2149d8"
d="m 208.61,125 c 0,-1.78 -0.06,-3.55 -0.13,-5.31 -2.561,-0.68 -5.184,-1.095 -7.83,-1.24 -4.737,-13.019 -13.862,-23.992 -25.799,-31.023 -11.937,-7.031 -25.958,-9.692 -39.641,-7.522 -13.683,2.169 -26.193,9.036 -35.37,19.415 -9.969,-3.375 -20.789,-3.296 -30.707,0.225 -9.918,3.52 -18.368,10.281 -23.978,19.185 -5.61,8.904 -8.061,19.444 -6.955,29.91 h -0.26 c -7.325,0 -14.358,2.913 -19.537,8.093 -5.18,5.179 -8.093,12.212 -8.093,19.537 0,7.325 2.913,14.358 8.093,19.537 5.179,5.18 12.212,8.093 19.537,8.093 H 179 c 19.116,-21.827 29.646,-49.885 29.61,-78.9 z"
stroke-width="1"
id="path108" />
<path
fill="#5c89f7"
d="m 99.84,99.32 c -9.969,-3.375 -20.789,-3.296 -30.707,0.225 -9.918,3.52 -18.368,10.281 -23.978,19.185 -5.61,8.904 -8.061,19.444 -6.955,29.91 h -0.26 c -7.157,0.025 -14.031,2.831 -19.161,7.821 -5.131,4.991 -8.126,11.785 -8.349,18.938 -0.223,7.154 2.343,14.121 7.153,19.421 4.809,5.301 11.496,8.529 18.637,9 30.996,-0.89 60.453,-13.84 82.064,-36.078 21.611,-22.238 33.713,-52.053 33.716,-83.062 0,-1.68 -0.06,-3.35 -0.13,-5 -2.427,-0.319 -4.872,-0.486 -7.32,-0.5 -8.455,-0.009 -16.815,1.782 -24.524,5.254 -7.709,3.473 -14.591,8.548 -20.186,14.886 z"
stroke-width="1"
id="path110" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
src/media/phones.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

29
src/pages/404.js Normal file
View file

@ -0,0 +1,29 @@
import React, {useRef, useEffect} from "react"
import Layout from "../components/layout"
import Hero from "../components/hero"
import bgImage from "../media/phones.png"
import { Link } from "gatsby"
const NotFoundPage = () => {
const firstBtn = useRef(null)
useEffect(() => {
firstBtn.current.focus()
}, [firstBtn])
return (
<Layout>
<Hero bg={bgImage}>
<h1 className="mb-5 text-5xl font-bold">Not found</h1>
<p className="mb-5 text-lg">Sorry 😔. We couldnt find what you were looking for.</p>
<Link ref={firstBtn} className="btn btn-primary" to="/">Home</Link>
</Hero>
</Layout>
)
}
export default NotFoundPage
export const Head = () => <title>Not found</title>

View file

@ -0,0 +1,24 @@
---
title: Creating your own JSON repository
---
# Creating your own JSON repository
Cloudstream uses JSON files to fetch and parse lists of repositories. You can create one following this template:
```json
{
"name": "<repository name>",
"description": "<repository description>",
"manifestVersion": 1,
"pluginLists": [
"<direct link to plugins.json>"
]
}
```
- `name`: self explanatory, will be visible in the app
- `description`: self explanatory, will be visible in the app
- `manifestVersion`: currently unused, may be used in the future for backwards compatibility
- `pluginLists`: List of urls, which contain plugins. All of them will be fetched.
- If you followed "[Using plugin template](../using-plugin-template.md)" tutorial, the appropriate `plugins.json` file should be in the builds branch of your new repository.
- If not, you can still generate one by running `gradlew makePluginsJson`

View file

@ -0,0 +1,73 @@
---
title: Creating your own providers
---
# Creating your own Providers
Providers in CloudStream consists primarily of 4 different parts:
- [Searching](/dokka/app/com.lagradost.cloudstream3/-main-a-p-i/index.html#498495168%2FFunctions%2F492899073)
- [Loading the home page](/dokka/app/com.lagradost.cloudstream3/-main-a-p-i/index.html#1356482668%2FFunctions%2F492899073)
- [Loading the show page](/dokka/app/com.lagradost.cloudstream3/-main-a-p-i/index.html#1671784382%2FFunctions%2F492899073)
- [Loading the video links](/dokka/app/com.lagradost.cloudstream3/-main-a-p-i/index.html#-930139416%2FFunctions%2F492899073)
When making a provider it is important that you are confident you can scrape the video links first!
Video links are often the most protected part of the website and if you cannot scrape them then the provider is useless.
## 0. Scraping
If you are unfamiliar with the concept of scraping, you should probably start by reading [this guide](../scraping/index.md) which should hopefuly familiarize you with this technique.
## 1. Searching
This one is probably the easiest, based on a query you should return a list of [SearchResponse](/dokka/app/com.lagradost.cloudstream3/-search-response/index.html)
Scraping the search results is essentially just finding the search item elements on the site (red box) and looking in them to find name, url and poster url and put the data in a SearchResponse.
![image](https://user-images.githubusercontent.com/46196380/184509999-0a50d13d-bc89-4f61-9f6e-f36648de0510.png)
The code for the search then ideally looks something like
```kotlin
// (code for Eja.tv)
override suspend fun search(query: String): List<SearchResponse> {
return app.post(
mainUrl, data = mapOf("search" to query) // Fetch the search data
).document // Convert the response to a searchable document
.select("div.card-body") // Only select the search items using a CSS selector
.mapNotNull { // Convert all html elements to SearchResponses and filter out the null search results
it.toSearchResponse()
}
}
// Converts a html element to a useable search response
// Basically just look in the element for sub-elements with the data you want
private fun Element.toSearchResponse(): LiveSearchResponse? {
// If no link element then it's no a valid search response
val link = this.select("div.alternative a").last() ?: return null
// fixUrl is a built in function to convert urls like /watch?v=..... to https://www.youtube.com/watch?v=.....
val href = fixUrl(link.attr("href"))
val img = this.selectFirst("div.thumb img")
// Optional parameter, scraping languages are not required but might be nice on some sites
val lang = this.selectFirst(".card-title > a")?.attr("href")?.removePrefix("?country=")
?.replace("int", "eu") //international -> European Union 🇪🇺
// There are many types of searchresponses but mostly you will be using AnimeSearchResponse, MovieSearchResponse
// and TvSeriesSearchResponse, all with different parameters (like episode count)
return LiveSearchResponse(
// Kinda hack way to get the title
img?.attr("alt")?.replaceFirst("Watch ", "") ?: return null,
href,
this@EjaTv.name,
TvType.Live,
fixUrl(img.attr("src")),
lang = lang
)
}
```
In this code snippet I have separated the Element to SearchResult conversion to a separate function because that function can often be used when scraping the home page later. No need to parse the same type of element twice.
# TODO REST

View file

@ -0,0 +1,8 @@
---
title: For extension developers
---
# For extension developers
This section will outline how to start developing your own extensions for Cloudstream.
You should probably start by reading [How to use our plugin template](../using-plugin-template.md).

View file

@ -0,0 +1,102 @@
---
title: Devtools detector
---
**TL;DR**: You are going to get fucked by sites detecting your devtools, the easiest bypass for this is using [a web sniffer extension](https://chrome.google.com/webstore/detail/web-sniffer/ndfgffclcpdbgghfgkmooklaendohaef?hl=en)
Many sites use some sort of debugger detection to prevent you from looking at the important requests made by the browser.
You can test the devtools detector [here](https://blog.aepkill.com/demos/devtools-detector/)
Code for the detector found [here](https://github.com/AEPKILL/devtools-detector)
# How are they detecting the tools?
One or more of the following methods are used to prevent devtools in the majority of cases (if not all):
**1.**
Calling `debugger` in an endless loop.
This is very easy to bypass. You can either right click the offending line (in chrome) and disable all debugger calls from that line or you can disable the whole debugger.
**2.**
Attaching a custom `.toString()` function to an expression and printing it with `console.log()`.
When devtools are open (even while not in console) all `console.log()` calls will be resloved and the custom `.toString()` function will be called. Functions can also be triggered by how dates, regex and functions are formatted in the console.
This lets the site know the millisecond you bring up devtools. Doing `const console = null` and other js hacks have not worked for me (the console function gets cached by the detector).
If you can find the offending js responsible for the detection you can bypass it by redifining the function in violentmonkey, but I recommend against it since it's often hidden and obfuscated. The best way to bypass this issue is to re-compile firefox or chrome with a switch to disable the console.
**3.**
Running a `while (true) {}` loop when the debugger object is present? Looks something like this in the wild:
```js
function _0x39426c(e) {
function t(e) {
if ("string" == typeof e)
return function(e) {}
.constructor("while (true) {}").apply("counter");
1 !== ("" + e / e).length || e % 20 == 0 ? function() {
return !0;
}
.constructor("debugger").call("action") : function() {
return !1;
}
.constructor("debugger").apply("stateObject"),
t(++e);
}
try {
if (e)
return t;
t(0);
} catch (e) {}
}
setInterval(function() {
_0x39426c();
}, 4e3);
```
This function can be tracked down to this [script](https://github.com/javascript-obfuscator/javascript-obfuscator/blob/6de7c41c3f10f10c618da7cd96596e5c9362a25f/src/custom-code-helpers/debug-protection/templates/debug-protection-function/DebuggerTemplate.ts)
I do not actually know how this works, but the loop seems gets triggered in the presence of a debugger. Either way this instantly freezes the webpage in firefox and makes it very unresponsive in chrome and does not rely on `console.log()`. You could bypass this by doing `const _0x39426c = null` in violentmonkey, but this bypass is not doable with heavily obfuscated js.
# How to bypass the detection?
If you just want to see the network log that is possible with extensions, see [Web Sniffer](https://chrome.google.com/webstore/detail/web-sniffer/ndfgffclcpdbgghfgkmooklaendohaef?hl=en)
I tracked down the functions making devtools detection possible in the firefox source code and compiled a version which is undetectable by any of these tools.
- [Linux build](https://mega.nz/file/YSAESJzb#x036cCtphjj9kB-kP_EXReTTkF7L7xN8nKw6sQN7gig)
- [Windows build](https://mega.nz/file/ZWAURAyA#qCrJ1BBxTLONHSTdE_boXMhvId-r0rk_kuPJWrPDiwg)
- [Mac build](https://mega.nz/file/Df5CRJQS#azO61dpP0_xgR8k-MmHaU_ufBvbl8_DlYky46SNSI0s)
- about:config `devtools.console.bypass` disables the console which invalidates **method 2**.
- about:config `devtools.debugger.bypass` completely disables the debugger, useful to bypass **method 3**.
If you want to compile firefox yourself with these bypasses you can, using the line changes below in the described files.
**BUILD: 101.0a1 (2022-04-19)**
`./devtools/server/actors/thread.js`
At line 390
```js
attach(options) {
let devtoolsBypass = Services.prefs.getBoolPref("devtools.debugger.bypass", true);
if (devtoolsBypass)
return;
```
`./devtools/server/actors/webconsole/listeners/console-api.js`
At line 92
```js
observe(message, topic) {
let devtoolsBypass = Services.prefs.getBoolPref("devtools.console.bypass", true);
if (!this.handler || devtoolsBypass) {
return;
}
```
`./browser/app/profile/firefox.js`
At line 23
```js
// Bypasses
pref("devtools.console.bypass", true);
pref("devtools.debugger.bypass", true);
```
### Next up: [Why your requests fail](../disguising_your_scraper.md)

View file

@ -0,0 +1,193 @@
---
title: Disguishing your scrapers
---
# Disguishing your scrapers
<p align="center">
If you're writing a <b>Selenium</b> scraper, be aware that your skill level doesn't match the minimum requirements for this page.
</p>
## Why is scraping not appreciated?
- It obliterates ads and hence, blocks the site revenue.
- It is more than usually used to spam the content serving networks, hence, affecting the server performance.
- It is more than usually also used to steal content off of a site and serve in other.
- Competent scrapers usually look for exploits on site. Among these, the open source scrapers may leak site exploits to a wider audience.
## Why do you need to disguise your scraper?
Like the above points suggest, scraping is a good act. There are mechanisms to actively kill scrapers and only allow the humans in. You will need to make your scraper's identity as narrow as possible to a browser's identity.
Some sites check the client using headers and on-site javascript challenges. This will result in invalid responses along the status code of 400-499.
*Keep in mind that there are sites that produce responses without giving out the appropriate status codes.*
## Custom Headers
Here are some headers you need to check for:
| Header | What's the purpose of this? | What should I change this to? |
| --- | --- | --- |
| `User-Agent` | Specifies your client's name along with the versions. | Probably the user-agent used by your browser. |
| `Referer` | Specifies which site referred the current site. | The url from which **you** obtained the scraping url. |
| `X-Requested-With` | Specifies what caused the request to that site. This is prominent in site's AJAX / API. | Usually: `XMLHttpRequest`, it may vary based on the site's JS |
| `Cookie` | Cookie required to access the site. | Whatever the cookie was when you accessed the site in your normal browser. |
| `Authorization` | Authorization tokens / credentials required for site access. | Correct authorization tokens or credentials for site access. |
Usage of correct headers will give you site content access given you can access it through your web browser.
**Keep in mind that this is only the fraction of what the possible headers can be.**
## Appropriate Libraries
In Python, `requests` and `httpx` have differences.
```py
>>> import requests, httpx
>>> requests.get("http://www.crunchyroll.com/", headers={"User-Agent": "justfoolingaround/1", "Referer": "https://example.com/"})
<Response [403]>
>>> httpx.get("http://www.crunchyroll.com/", headers={"User-Agent": "justfoolingaround/1", "Referer": "https://example.com/"})
<Response [200 OK]>
```
As we can see, the former response is a 403. This is a forbidden response and generally specifies that the content is not present. The latter however is a 200, OK response. In this response, content is available.
This is the result of varying internal mechanisms.
The only cons to `httpx` in this case might be the fact that it has fully encoded headers, whilst `requests` does not. This means header keys consisting of non-ASCII characters may not be able to bypass some sites.
## Response handling algorithms
A session class is an object available in many libraries. This thing is like a house for your outgoing requests and incoming responses. A well written library has a session class that even accounts for appropriate cookie handling. Meaning, if you ever send a request to a site you need not need to worry about the cookie of that site for the next site you visit.
No matter how cool session classes may be, at the end of the day, they are mere objects. That means, you, as a user can easily change what is within it. (This may require a high understanding of the library and the language.)
This is done through inheritance. You inherit a session class and modify whats within.
For example:
```py
class QuiteANoise(httpx.Client):
def request(self, *args, **kwargs):
print("Ooh, I got a request with arguments: {!r}, and keyword arguments: {!r}.".format(args, kwargs))
response = super().request(*args, **kwargs)
print("That request has a {!r}!".format(response))
return response
```
In the above inherited session, what we do is *quite noisy*. We announced a request that is about to be sent and a response that was just recieved.
`super`, in Python, allows you to get the class that the current class inherits.
Do not forget to return your `response`, else your program will be dumbfounded since nothing ever gets out of your request!
So, we're going to abuse this fancy technique to effectively bypass some hinderances.
Namely `hCaptcha`, `reCaptcha` and `Cloudflare`.
```py
"""
This code is completely hypothetical, you probably
do not have a hCaptcha, reCaptcha and a Cloudflare
bypass.
This code is a mere reference and may not suffice
your need.
"""
from . import hcaptcha
from . import grecaptcha
import httpx
class YourScraperSession(httpx.Client):
def request(self, *args, **kwargs):
response = super().request(*args, **kwargs)
if response.status_code >= 400:
if hcaptcha.has_cloudflare(response):
cloudflare_cookie = hcaptcha.cloudflare_clearance_jar(self, response, *args, **kwargs)
self.cookies.update(cloudflare_cookie)
return self.request(self, *args, **kwargs)
# Further methods to bypass something else.
return self.request(self, *args, **kwargs) # psssssst. RECURSIVE HELL, `return response` is safer
hcaptcha_sk, type_of = hcaptcha.deduce_sitekey(self, response)
if hcaptcha_sk:
if type_of == 'hsw':
token = hcaptcha.get_hsw_token(self, response, hcaptcha_sk)
else:
token = hcaptcha.get_hsl_token(self, response, hcaptcha_sk)
setattr(response, 'hcaptcha_token', token)
recaptcha_sk, type_of = grecaptcha.sitekey_on_site(self, response)
if recaptcha_sk:
if isinstance(type_of, int):
token = grecaptcha.recaptcha_solve(self, response, recaptcha_sk, v=type_of)
else:
token = type_of
setattr(response, 'grecaptcha_token', token)
return response
```
So, let's see what happens here.
Firstly, we check whether the response has a error or not. This is done by checking if the response's status code is **greater than or equal to** 400.
After this, we check if the site has Cloudflare, if the site has Cloudflare, we let the hypothetical function do its magic and give us the bypass cookies. Then after, we update our session class' cookie. Cookie vary across sites but in this case, our hypothetical function will take the session and make it so that the cookie only applies to that site url within and with the correct headers.
After a magical cloudflare bypass (people wish they have this, you will too, probably.), we call the overridden function `.request` again to ensure the response following this will be bypassed to. This is recursion.
If anything else is required, you should add your own code to execute bypasses so that your responses will be crisp and never error-filled.
Else, we just return the fresh `.request`.
Keep in mind that if you cannot bypass the 400~ error, your responses might end up in a permanent recursive hell, at least in the code above.
To not make your responses never return, you might want to return the non-bypassed response.
The next part mainly focuses on CAPTCHA bypasses and what we do is quite simple. A completed CAPTCHA *usually* returns a token.
Returning this token with the response is not a good idea as the entire return type will change. We use a sneaky little function here. Namely `setattr`. What this does is, it sets an attribute of an object.
The algorithm in easier terms is:
Task: Bypass a donkey check with your human.
- Yell "hee~haw". (Prove that you're a donkey, this is how the hypothetical functions work.)
- Be handed the ribbon. (In our case, this is the token.)
Now the problem is, the ribbon is not a human but still needs to come back. How does a normal human do this? Wear the ribbon.
Wearing the ribbon is `setattr`. We can wear the ribbon everywhere. Leg, foot, butt.. you name it. No matter where you put it, you get the ribbon, so just be a bit reasonable with it. Like a decent developer and a decent human, wear the ribbon on the left side of your chest. In the code above, this reasonable place is `<captcha_name>_token`.
Let's get out of this donkey business.
After this reasonable token placement, we get the response back.
This token can now, always be accessed in reasonable places, reasonably.
```py
client = YourScraperSession()
bypassed_response = client.get("https://kwik.cx/f/2oHQioeCvHtx")
print(bypassed_response.hcaptcha_token)
```
Keep in mind that if there is no ribbon/token, there is no way of reasonably accessing it.
In any case, this is how you, as a decent developer, handle the response properly.
### Next up: [Finding video links](../finding_video_links.md)

View file

@ -0,0 +1,65 @@
---
title: Finding video links
---
# Finding video links
Now you know the basics, enough to scrape most stuff from most sites, but not streaming sites.
Because of the high costs of video hosting the video providers really don't want anyone scraping the video and bypassing the ads.
This is why they often obfuscate, encrypt and hide their links which makes scraping really hard.
Some sites even put V3 Google Captcha on their links to prevent scraping while the majority IP/time/referer lock the video links to prevent sharing.
You will almost never find a plain `<video>` element with a mp4 link.
**This is why you should always scrape the video first when trying to scrape a video hosting site. Sometimes getting the video link can be too hard.**
I will therefore explain how to do more advanced scraping, how to get these video links.
What you want to do is:
1. Find the iFrame/Video host.*
2. Open the iFrame in a separate tab to ease clutter.*
3. Find the video link.
4. Work backwards from the video link to find the source.
* *Step 1 and 2 is not applicable to all sites.*
Let's explain further:
**Step 1**: Most sites use an iFrame system to show their videos. This is essentially loading a separate page within the page.
This is most evident in [Gogoanime](https://gogoanime.gg/yakusoku-no-neverland-episode-1), link gets updated often, google the name and find their page if link isn't found.
The easiest way of spotting these iframes is looking at the network tab trying to find requests not from the original site. I recommend using the HTML filter.
![finding](https://user-images.githubusercontent.com/46196380/149821806-7426ca0f-133f-4722-8e7f-ebae26ea2ef1.png)
Once you have found the iFrame, in this case a fembed-hd link open it in another tab and work from there. (**Step 2**)
If you only have the iFrame it is much easier to find the necessary stuff to generate the link since a lot of useless stuff from the original site is filtered out.
**Step 3**: Find the video link. This is often quite easy, either filter all media requests or simply look for a request ending in .m3u8 or .mp4
What this allows you to do is limit exclude many requests (only look at the requests before the video link) and start looking for the link origin (**Step 4**).
![video_link](https://user-images.githubusercontent.com/46196380/149821919-f65e2f72-b413-4151-a4a3-db7012e2ed18.png)
I usually search for stuff in the video link and see if any text/headers from the preceding requests contain it.
In this case fvs.io redirected to the mp4 link, now do the same steps for the fvs.io link to follow the request backwards to the origin. Like images are showing.
![fvs](https://user-images.githubusercontent.com/46196380/149821967-00c01103-5b4a-48dd-be18-e1fdfb967e4c.png)
![fvs_redirector](https://user-images.githubusercontent.com/46196380/149821984-0720addd-40a7-4a9e-a429-fec45ec28901.png)
![complete](https://user-images.githubusercontent.com/46196380/149821989-49b2ba8c-36b1-49a7-a41b-3c69df278a9f.png)
**NOTE: Some sites use encrypted JS to generate the video links. You need to use the browser debugger to step by step find how the links are generated in that case**
## **What to do when the site uses a captcha?**
You pretty much only have 3 options when that happens:
1. Try to use a fake / no captcha token. Some sites actually doesn't check that the captcha token is valid.
2. Use Webview or some kind of browser in the background to load the site in your stead.
3. Pray it's a captcha without payload, then it's possible to get the captcha key without a browser: [Code example](https://github.com/recloudstream/cloudstream/blob/ccb38542f4b5685e511824a975bf16190011c222/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt#L132-L181)

View file

@ -0,0 +1,19 @@
---
title: Scraping tutorial
---
# Requests based scraping tutorial
You want to start scraping? Well this guide will teach you, and not some baby selenium scraping. This guide only uses raw requests and has examples in both python and kotlin. Only basic programming knowlege in one of those languages is required to follow along in the guide.
If you find any aspect of this guide confusing please open an issue about it and I will try to improve things.
If you do not know programming at all then this guide will __not__ help you, learn programming! first Real scraping cannot be done by copy pasting with a vauge understanding.
0. [Starting scraping from zero](../starting.md)
1. [Properly scraping JSON apis often found on sites](../using_apis.md)
2. [Evading developer tools detection when scraping](../devtools_detectors.md)
3. [Why your requests fail and how to fix them](../disguising_your_scraper.md)
4. [Finding links and scraping videos](../finding_video_links.md)
Once you've read and understood the concepts behind scraping take a look at [a provider for CloudStream](https://github.com/recloudstream/cloudstream-extensions/blob/master/VidstreamBundle/src/main/kotlin/com/lagradost/VidEmbedProvider.kt#L4). I added tons of comments to make every aspect of writing CloudStream providers clear. Even if you're not planning on contributing to Cloudstream looking at the code may help.

View file

@ -0,0 +1,222 @@
---
title: Starting
---
Scraping is just downloading a webpage and getting the wanted information from it.
As a start you can scrape the README.md
I'll use khttp for the kotlin implementation because of the ease of use, if you want something company-tier I'd recommend OkHttp.
**Update**: I have made an okhttp wrapper **for android apps**, check out [NiceHttp](https://github.com/Blatzar/NiceHttp)
# **1. Scraping the Readme**
**Python**
```python
import requests
url = "https://recloudstream.github.io/devs/scraping/"
response = requests.get(url)
print(response.text) # Prints the readme
```
**Kotlin**
In build.gradle:
```gradle
repositories {
mavenCentral()
jcenter()
maven { url 'https://jitpack.io' }
}
dependencies {
// Other dependencies above
compile group: 'khttp', name: 'khttp', version: '1.0.0'
}
```
In main.kt
```kotlin
fun main() {
val url = "https://recloudstream.github.io/devs/scraping/"
val response = khttp.get(url)
println(response.text)
}
```
# **2. Getting the github project description**
Scraping is all about getting what you want in a good format you can use to automate stuff.
Start by opening up the developer tools, using
<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd>
or
<kbd>f12</kbd>
or
Right click and press *Inspect*
In here you can look at all the network requests the browser is making and much more, but the important part currently is the HTML displayed. You need to find the HTML responsible for showing the project description, but how?
Either click the small mouse in the top left of the developer tools or press
<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>C</kbd>
This makes your mouse highlight any element you hover over. Press the description to highlight up the element responsible for showing it.
Your HTML will now be focused on something like:
```html
<p class="f4 mt-3">
Work in progress tutorial for scraping streaming sites
</p>
```
Now there's multiple ways to get the text, but the 2 methods I always use is Regex and CSS selectors. Regex is basically a ctrl+f on steroids, you can search for anything. CSS selectors is a way to parse the HTML like a browser and select an element in it.
## CSS Selectors
The element is a paragraph tag, eg `<p>`, which can be found using the CSS selector: "p".
classes helps to narrow down the CSS selector search, in this case: `class="f4 mt-3"`
This can be represented with
```css
p.f4.mt-3
```
a dot for every class [full list of CSS selectors found here](https://www.w3schools.com/cssref/css_selectors.asp)
You can test if this CSS selector works by opening the console tab and typing:
```js
document.querySelectorAll("p.f4.mt-3");
```
This prints:
```java
NodeList [p.f4.mt-3]
```
### **NOTE**: You may not get the same results when scraping from command line, classes and elements are sometimes created by javascript on the site.
**Python**
```python
import requests
from bs4 import BeautifulSoup # Full documentation at https://www.crummy.com/software/BeautifulSoup/bs4/doc/
url = "https://github.com/Blatzar/scraping-tutorial"
response = requests.get(url)
soup = BeautifulSoup(response.text, 'lxml')
element = soup.select("p.f4.mt-3") # Using the CSS selector
print(element[0].text.strip()) # Selects the first element, gets the text and strips it (removes starting and ending spaces)
```
**Kotlin**
In build.gradle:
```gradle
repositories {
mavenCentral()
jcenter()
maven { url 'https://jitpack.io' }
}
dependencies {
// Other dependencies above
implementation "org.jsoup:jsoup:1.11.3"
compile group: 'khttp', name: 'khttp', version: '1.0.0'
}
```
In main.kt
```kotlin
fun main() {
val url = "https://github.com/Blatzar/scraping-tutorial"
val response = khttp.get(url)
val soup = Jsoup.parse(response.text)
val element = soup.select("p.f4.mt-3") // Using the CSS selector
println(element.text().trim()) // Gets the text and strips it (removes starting and ending spaces)
}
```
## **Regex:**
When working with Regex I highly recommend using (regex101.com)[https://regex101.com/] (using the python flavor)
Press <kbd>Ctrl</kbd> + <kbd>U</kbd>
to get the whole site document as text and copy everything
Paste it in the test string in regex101 and try to write an expression to only capture the text you want.
In this case the elements is
```html
<p class="f4 mt-3">
Work in progress tutorial for scraping streaming sites
</p>
```
Maybe we can search for `<p class="f4 mt-3">` (backslashes for ")
```regex
<p class=\"f4 mt-3\">
```
Gives a match, so lets expand the match to all characters between the two brackets ( p>....</ )
Some important tokens for that would be:
- `.*?` to indicate everything except a newline any number of times, but take as little as possible
- `\s*` to indicate whitespaces except a newline any number of times
- `(*expression inside*)` to indicate groups
Which gives:
```regex
<p class=\"f4 mt-3\">\s*(.*)?\s*<
```
**Explained**:
Any text exactly matching `<p class="f4 mt-3">`
then any number of whitespaces
then any number of any characters (which will be stored in group 1)
then any number of whitespaces
then the text `<`
In code:
**Python**
```python
import requests
import re # regex
url = "https://github.com/Blatzar/scraping-tutorial"
response = requests.get(url)
description_regex = r"<p class=\"f4 mt-3\">\s*(.*)?\s*<" # r"" stands for raw, which makes blackslashes work better, used for regexes
description = re.search(description_regex, response.text).groups()[0]
print(description)
```
**Kotlin**
In main.kt
```kotlin
fun main() {
val url = "https://github.com/Blatzar/scraping-tutorial"
val response = khttp.get(url)
val descriptionRegex = Regex("""<p class="f4 mt-3">\s*(.*)?\s*<""")
val description = descriptionRegex.find(response.text)?.groups?.get(1)?.value
println(description)
}
```
### Next up: [Properly scraping JSON apis](../using_apis.md)

View file

@ -0,0 +1,171 @@
---
title: Using APIs
---
### About
Whilst scraping a site is always a nice option, using it's API is way better. <br/>
And sometimes its the only way `(eg: the site uses its API to load the content, so scraping doesn't work)`.
Anyways, this guide won't teach the same concepts over and over again, <br/>
so if you can't even make requests to an API then this will not tell you how to do that.
Refer to [starting](../starting.md) on how to make http/https requests.
And yes, this guide expects you to have basic knowledge on both Python and Kotlin.
### Using an API (and parsing json)
So, the API I will use is the [SWAPI](https://swapi.dev/). <br/>
To parse that json data in python you would do:
```python
import requests
url = "https://swapi.dev/api/planets/1/"
json = requests.get(url).json()
""" What the variable json looks like
{
"name": "Tatooine",
"rotation_period": "23",
"orbital_period": "304",
"diameter": "10465",
"climate": "arid",
"gravity": "1 standard",
"terrain": "desert",
"surface_water": "1",
"population": "200000",
"residents": [
"https://swapi.dev/api/people/1/"
],
"films": [
"https://swapi.dev/api/films/1/"
],
"created": "2014-12-09T13:50:49.641000Z",
"edited": "2014-12-20T20:58:18.411000Z",
"url": "https://swapi.dev/api/planets/1/"
}
"""
```
Now, that is way too simple in python, sadly I am here to get your hopes down, and say that its not as simple in kotlin. <br/>
First of all, we are going to use a library named Jackson by FasterXML. <br/>
In build.gradle:
```gradle
repositories {
mavenCentral()
jcenter()
maven { url 'https://jitpack.io' }
}
dependencies {
...
...
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.11.3"
compile group: 'khttp', name: 'khttp', version: '1.0.0'
}
```
After we have installed the dependencies needed, we have to define a schema for the json. <br/>
Essentially, we are going to write the structure of the json in order for jackson to parse our json. <br/>
This is an advantage for us, since it also means that we get the nice IDE autocomplete/suggestions and typehints! <br/><br/>
Getting the json data:
```kotlin
val jsonString = khttp.get("https://swapi.dev/api/planets/1/").text
```
First step is to build a mapper that reads the json string, in order to do that we need to import some things first.
```kotlin
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.readValue
```
After that we initialize the mapper:
```kotlin
val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
```
The next step is to...write down the structure of our json!
This is the boring part for some, but it can be automated by using websites like [json2kt](https://www.json2kt.com/) or [quicktype](https://app.quicktype.io/) to generate the entire code for you.
<br/><br/>
First step to declaring the structure for a json is to import the JsonProperty annotation.
```kotlin
import com.fasterxml.jackson.annotation.JsonProperty
```
Second step is to write down a data class that represents said json.
```kotlin
// example json = {"cat": "meow", "dog": ["w", "o", "o", "f"]}
data class Example (
@JsonProperty("cat") val cat: String,
@JsonProperty("dog") val dog: List<String>
)
```
This is as simple as it gets. <br/> <br/>
Enough of the examples, this is the representation of `https://swapi.dev/api/planets/1/` in kotlin:
```kotlin
data class Planet (
@JsonProperty("name") val name: String,
@JsonProperty("rotation_period") val rotationPeriod: String,
@JsonProperty("orbital_period") val orbitalPeriod: String,
@JsonProperty("diameter") val diameter: String,
@JsonProperty("climate") val climate: String,
@JsonProperty("gravity") val gravity: String,
@JsonProperty("terrain") val terrain: String,
@JsonProperty("surface_water") val surfaceWater: String,
@JsonProperty("population") val population: String,
@JsonProperty("residents") val residents: List<String>,
@JsonProperty("films") val films: List<String>,
@JsonProperty("created") val created: String,
@JsonProperty("edited") val edited: String,
@JsonProperty("url") val url: String
)
```
**For json that don't necessarily contain a key, or its type can be either the expected type or null, you need to write that type as nullable in the representation of that json.** <br/>
Example of the above situation:
```json
[
{
"cat":"meow"
},
{
"dog":"woof",
"cat":"meow"
},
{
"fish":"meow",
"cat":"f"
}
]
```
It's representation would be:
```kotlin
data class Example (
@JsonProperty("cat") val cat: String,
@JsonProperty("dog") val dog: String?,
@JsonProperty("fish") val fish: String?
)
```
As you can see, `dog` and `fish` are nullable because they are properties that are missing in an item. <br/>
Whilst `cat` is not nullable because it is available in all of the items. <br/>
Basic nullable detection is implemented in [json2kt](https://www.json2kt.com/) so its recommended to use that. <br/>
But it is very likely that it might fail to detect some nullable types, so it's up to us to validate the generated code.
Second step to parsing json is...to just call our `mapper` instance.
```kotlin
val json = mapper.readValue<Planet>(jsonString)
```
And voila! <br/>
We have successfully parsed our json within kotlin. <br/>
One thing to note is that you don't need to add all of the json key/value pairs to the structure, you can just have what you need.
### Note
Even though we set `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES` as `false` it will still error on missing properties. <br/>
If a json may or may not include some info, make those properties as nullable in the structure you build.
### Next up: [Evading developer tools detection](../devtools_detectors.md)

View file

@ -0,0 +1,21 @@
---
title: Using plugin template
---
# Using the template
The easiest way to start developing is to use our [plugin-template](https://github.com/recloudstream/plugin-template) as it sets up the build environment automatically.
1) <a href="https://github.com/recloudstream/plugin-template/generate" target="_blank">Use our plugin template</a>
2) Select `Include all branches`: <!-- - ![Include all branches image](../use-template.png) -->
3) Check if GitHub actions are enabled, by going to: `Settings > Actions > General > Allow all actions and reusable workflows`
4) You can now create your own plugins and after you push new code, they should automatically be built
### Further reading
After you have set up the repository, you can continue by reading:
- [Creating your own JSON repository](../create-your-own-json-repository.md)
- [Creating your own providers](../create-your-own-providers.md)

35
src/pages/docs/index.js Normal file
View file

@ -0,0 +1,35 @@
import { graphql, StaticQuery } from "gatsby"
import React from "react"
import Layout from "../../components/layout"
import DocsCard from "../../components/cards/docs"
const DocsPage = () => {
return <Layout>
<div className="flex items-center w-full flex-col">
<StaticQuery
query={graphql`
query {
allMarkdownRemark {
nodes {
frontmatter {
title
}
excerpt
fileAbsolutePath
}
}
}
`}
render={data => (
<>{data.allMarkdownRemark.nodes.map(it => {
return <DocsCard md={it} key={it} />
})}</>
)}
/>
</div>
</Layout>
}
export default DocsPage

View file

@ -0,0 +1,68 @@
---
title: For users
---
# Cloudstream
An Android app to stream and download Movies, TV-Shows and Animes
This [open source app](https://github.com/recloudstream/cloudstream) can stream any media content from various sources
### Disclaimer ⚠️
The app is purely for educational and personal use.
CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface.
It merely scrapes 3rd-party websites that are publicly accessible via any regular web browser.
It is the responsibility of the user to avoid any actions that might violate the laws governing his or her locality.
### What are the risks in practice?
Basically none. The content is hosted by the "providers", and if you don't distribute the content yourself, you are pretty much safe
A lot of misleading articles online try to scare people into buying a VPN subscription when the authors are earning a big commission (sometimes 100% of the VPN subscription).
VPNs are useful for certain things but aren't a magical tool and most people don't need those
## Features
### Streaming
You can stream media from various sources (developed by the community)
### Downloading
You can download media (movies, series and animes) files anywhere on your phone or external storage
### Searching
When you search for a media, Cloudstream will search every provider to see if it has that media or not.
You can change the list of used providers with the settings button in the search page
You can also enable foreign language providers (such as French) under:
Settings page > Provider Languages
and tick the ones you want there
### How does it work ?
Basically this app sends requests to different websites (also called sources or providers) to find movies, series and animes and display them in the app nicely
### 100% free and no ads!
Cloudstream-3 (also refered to as Cs3) is open-source, that means that anyone can look at the code, their isn't any invasive data collection and anyone can create their own modifed version
This app doesn't contain any ads and is completely free to download, as a result this app doesn't make any money from someone using the app.
But you can still help it's development:
- By contributing to the code of the app itself (requires knowledge of Kotlin)
- By translating the app in another language
- By adding a star to the project on github
- By donating money to dependencies of the project (requires money) *The devs of Cloudstream-3 don't accept donations*
## Availability
You can download this app on Android, Android TV, and FireTV Stick. However, it is unfortunately not avaliable on IOS or on desktop, but you can use tools like:
- [WSA](https://docs.microsoft.com/en-us/windows/android/wsa/) on Windows
- [Waydroid](https://waydro.id/) or [Anbox](https://anbox.io/) on Linux
to emulate Android on desktop.

View file

@ -0,0 +1,25 @@
---
title: Using local extensions
---
It is possible to use extensions without using repositories. This can be useful when prototyping.
## To install an extension locally:
### 1. Give Cloudstream storage permissions
if you're using Android 11 or later you need to make sure that you **Allow management of all files**, not just media in the settings.
### 2. Start and close the app
If you've done everything correctly, you should now see a `Cloudstream3` folder in your device's files. Inside it there should be a `plugins` folder.
### 3. Download or compile extensions
You can now download (most likely from the `builds` branch of the repository of your choosing) or compile an extension. You should look for files ending with `.cs3`
### 4. Put your newly acquired `.cs3` file in the `Cloudstream3/plugins` folder.
### 5. You should now see your extension working.
You can make sure by going to `settings > extensions` and clicking the bottom progress bar to show a list of all extensions (including the locally installed ones)

38
src/pages/index.js Normal file
View file

@ -0,0 +1,38 @@
import React, {useEffect, useRef} from "react"
import Layout from "../components/layout"
import Hero from "../components/hero"
import bgImage from "../media/phones.png"
import { Link } from "gatsby"
const IndexPage = () => {
const firstBtn = useRef(null)
useEffect(() => {
firstBtn.current.focus()
}, [firstBtn])
return <Layout>
<Hero bg={bgImage}>
<h1 className="mb-5 text-5xl font-bold">Hello there</h1>
<p className="mb-5 text-lg">Cloudstream is an Android app for streaming and downloading Movies, TV-Series and Anime.</p>
<div className="flex justify-center w-full mb-5">
<Link ref={firstBtn} className="btn btn-primary" to="/install">Install</Link>
<div className="divider divider-horizontal" />
<Link className="btn btn-primary" to="/repos">Repositories</Link>
</div>
<Link to="/docs" className="link">Documentation</Link>
</Hero>
</Layout>
}
export default IndexPage
export function Head() {
return (
<title>Cloudstream</title>
)
}

83
src/pages/install.js Normal file
View file

@ -0,0 +1,83 @@
import React, { useEffect, useState } from "react"
import Layout from "../components/layout"
import Hero from "../components/hero"
import bgImage from "../media/phones.png"
const InstallPage = () => {
const [data, setData] = useState(null)
useEffect(() => {
fetch("https://api.github.com/repos/recloudstream/cloudstream/releases")
.then(r => r.json())
.then(r => {
let btns = []
let nonPre = false
let Pre = false;
for (const entry of r) {
if (Pre && nonPre) continue;
console.log({Pre, nonPre, entry})
var apk = null;
for (const asset of entry.assets) {
if (asset.content_type !== "application/vnd.android.package-archive") continue;
apk = asset;
}
if (!apk) continue;
if (entry.prerelease || entry.tag_name === "pre-release") {
if (!Pre) {
btns.push({
pre: true,
url: entry.html_url,
apk: apk,
tag: entry.tag_name
})
Pre = true
}
} else if (!nonPre) {
btns.push({
pre: false,
url: entry.html_url,
apk: apk,
tag: entry.tag_name
})
nonPre = true
}
}
setData({ btns })
})
}, [setData])
return <Layout>
<Hero bg={bgImage}>
<h1 className="mb-5 text-5xl font-bold">Installation</h1>
{(data != null) &&
<div className="flex flex-col items-center gap-3">{
data.btns.map(it => {
return <div className="btn-group" key={JSON.stringify(it)}>
<button className={"btn " + (it.pre?'btn-secondary':'btn-primary')} onClick={() => {
window.open(it.apk.browser_download_url)
}}>Download {it.tag}</button>
<button className="btn"onClick={() => {
window.open(it.url)
}}>Release notes</button>
</div>
})}
</div>
}
{(data == null) &&
<a href="https://github.com/recloudstream/cloudstream/releases/download/pre-release/app-prerelease.apk" className="btn btn-primary">Download pre-release</a>
}
</Hero>
</Layout>
}
export function Head() {
return (
<title>Cloudstream install</title>
)
}
export default InstallPage

33
src/pages/repos.js Normal file
View file

@ -0,0 +1,33 @@
import React, {useEffect, useState} from "react"
import Layout from "../components/layout"
import RepoCard from "../components/cards/repo"
const IndexPage = () => {
const [repos, setRepos] = useState([]);
useEffect(() => {
fetch("https://raw.githubusercontent.com/recloudstream/cs-repos/master/repos-db.json")
.then(r => r.json())
.then(data => {
setRepos(data)
})
}, [setRepos])
return <Layout>
<div className="flex items-center w-full flex-col">
{repos &&
repos.map((it, index) => <RepoCard url={it} key={index} isFirst={index===0}/>)
}
</div>
</Layout>
}
export function Head() {
return (
<title>Cloudstream repos</title>
)
}
export default IndexPage

77
src/templates/default.jsx Normal file
View file

@ -0,0 +1,77 @@
import React from "react"
import { Link, graphql } from "gatsby"
import Layout from "../components/layout"
import rehypeReact from "rehype-react"
function DivMixin(props) {
console.log(props)
if (props?.class != null && props.class.includes("gatsby-highlight")) {
props.class = props.class + " mockup-code mb-5"
}
return <div {...props} />
}
const componentMap = {
div: DivMixin,
a: (props) => {
return <Link className="link" to={props?.href} {...props} />
},
ol: (props) => {
return <ul className="steps steps-vertical" {...props} />
},
li: (props) => {
return <li className="step" {...props} />
},
hr: (props) => {
return <div class="divider" {...props} />
}
}
export default function PageTemplate({ data: { markdownRemark } }) {
const renderAst = new rehypeReact({
createElement: React.createElement,
components: componentMap,
Fragment: ({ children }) => {
return <div className="prose contents">{children}</div>
}
}).Compiler
return (
<Layout>
<div className="flex items-center w-full flex-col">
<div class="text-sm breadcrumbs md-5">
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/docs">Documentation</Link></li>
<li>{markdownRemark.frontmatter.title}</li>
</ul>
</div>
<div className="w-full mx-10 md:w-2/3 card bg-base-200 shadow-xl">
<div className="card-body">
<h2 class="card-title">{markdownRemark.frontmatter.title}</h2>
{renderAst(markdownRemark.htmlAst)}
</div>
</div>
</div>
</Layout>
)
}
export function Head({data}) {
return (
<title>{data.markdownRemark.frontmatter.title}</title>
)
}
export const pageQuery = graphql`
query ($id: String!) {
markdownRemark(id: { eq: $id }) {
frontmatter {
title
}
htmlAst
}
}
`

7
src/utils.js Normal file
View file

@ -0,0 +1,7 @@
function node2slug(node) {
return node.fileAbsolutePath.replace(/^.+\/docs/g, "/docs")
}
module.exports = {
node2slug: node2slug
}

10
tailwind.config.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = {
content: ['./src/**/*.{js,ts,jsx}'],
plugins: [require('daisyui'), require('@tailwindcss/typography')],
daisyui: {
themes: [
"dark"
]
}
};