mirror of
https://codeberg.org/ashley/poke
synced 2025-04-06 09:38:00 +00:00
Compare commits
No commits in common. "22.03-web" and "main" have entirely different histories.
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.yarn
|
||||||
|
.pnp*
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
node_modules
|
||||||
|
invidious-source
|
||||||
|
json.sqlite
|
||||||
|
*.md
|
||||||
|
docker-compose.yml
|
||||||
|
Dockerfile
|
19
.drone.yml
Normal file
19
.drone.yml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: exec
|
||||||
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Build
|
||||||
|
environment:
|
||||||
|
CODEBERG_USERNAME:
|
||||||
|
from_secret: CODEBERG_USERNAME
|
||||||
|
CODEBERG_PASSWORD:
|
||||||
|
from_secret: CODEBERG_PASSWORD
|
||||||
|
commands:
|
||||||
|
- echo $CODEBERG_PASSWORD | docker login codeberg.org --username $CODEBERG_USERNAME --password-stdin
|
||||||
|
- docker build -t codeberg.org/korbs/poke:arm64 .
|
||||||
|
- docker push codeberg.org/korbs/poke:arm64
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
|
.env
|
||||||
|
json.sqlite
|
169
CODE_OF_CONDUCT.md
Normal file
169
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
# Contributor Covenant Code of Conduct / PokeTube code of conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
iamashley@duck.com (E-mail) https://discord.gg/pfKSQ3pMfW (Discord server) https://matrix.to/#/#poke:vern.cc (matrix space) and https://rvlt.gg/poke (revolt server).
|
||||||
|
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Additional Terms for Poketube
|
||||||
|
**TL;DR**: You are encouraged not to edit or remove these terms from Poketube. While you have the freedom to make changes in your Poketube fork, if you choose to modify this document, please refrain from using the title "Poketube Code of Conduct." Everyone can copy and share this document as is, but making changes is allowed with the aforementioned condition. If your chosen alternative code of conduct doesn't include provisions against hate speech, inappropriate behavior, anti-immigrant sentiments, far-right, or authoritarian content, it's not recommended.
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
|
||||||
|
- **"Alternative Code of Conduct"**: This refers to a code of conduct other than the Contributor Covenant Code of Conduct.
|
||||||
|
|
||||||
|
- **"Free Software"**: The definition of "free software" is in accordance with the GNU GPL version 3. You can find a complete copy of it in the LICENSE file.
|
||||||
|
|
||||||
|
- **Hate Speech**: Hate speech includes any communication, whether written, spoken, or expressed in any form, that promotes discrimination, hostility, or violence against individuals or groups based on attributes such as race, ethnicity, gender, religion, or other protected characteristics.
|
||||||
|
|
||||||
|
- **Inappropriate Behavior**: Inappropriate behavior encompasses actions or expressions that create an unwelcome, hostile, or offensive environment for others, such as harassment, intimidation, or bullying.
|
||||||
|
|
||||||
|
- **Authoritarianism**: Authoritarianism is characterized by an emphasis on strong central authority, limited individual freedoms, and restrictions on democratic processes. Content or behavior that promotes authoritarian principles, suppresses freedom of speech, individual rights, or democratic values is strongly discouraged.
|
||||||
|
|
||||||
|
- **Protected characteristics** include attributes such as race, ethnicity, gender, religion, sexual orientation, disability, and other traits or qualities safeguarded from discrimination by relevant laws and regulations. This defines what is meant by "protected characteristics" in the context of this document.
|
||||||
|
|
||||||
|
2. Terms
|
||||||
|
|
||||||
|
NOTE: The Contributor Covenant Code of Conduct already includes provisions on some of these issues. Our intention is to provide a more defined and explicit statement regarding these prohibitions to ensure a clear and inclusive community environment.
|
||||||
|
|
||||||
|
YOU ARE NOT ENCOURAGED TO EDIT, REMOVE, OR ALTER THE TERMS OF THIS FILE. However, should you choose to make changes, please avoid using the title "Poketube Code of Conduct." Removing this file from your Poketube fork is allowed. Everyone, without exception, is permitted to create unmodified copies of this document and distribute it as is; however, modifications to this document are allowed with the aforementioned condition.
|
||||||
|
|
||||||
|
It is of paramount importance to emphasize that the promotion or glorification of anti-immigrant sentiments, the alignment with far-right ideologies, Islamophobia, or any form of religious discrimination is strongly discouraged within the scope of Poketube. We maintain a stance against such content, which includes material that discriminates against immigrants, promotes hatred or hostility towards religious groups, or actively supports extremist beliefs associated with far-right ideologies. This stance is encouraged and non-binding.
|
||||||
|
|
||||||
|
We believe in fostering an environment that is inclusive, respectful, and free from discrimination or the promotion of extremist ideologies. As such, any content found in violation of this encouragement will be addressed promptly and appropriately.
|
||||||
|
|
||||||
|
THE CLARITY AND FORCE OF THIS STATEMENT ARE INTENDED TO ENCOURAGE CLEAR GUIDELINES: ANTI-IMMIGRANT SENTIMENTS, FAR-RIGHT IDEOLOGIES, ISLAMOPHOBIA, RELIGIOUS DISCRIMINATION, MISOGYNY, AND SEXISM ARE STRONGLY DISCOURAGED AND NOT PREFERRED WITHIN OUR COMMUNITY. OUR HOPE IS TO MAINTAIN A RESPECTFUL AND INCLUSIVE ATMOSPHERE FOR ALL, REGARDLESS OF THEIR BACKGROUND, BELIEFS, OR IDENTITY.
|
||||||
|
|
||||||
|
These terms may be subject to change, and any updates will be communicated to the Poketube community. Changes to these terms will be communicated to users.
|
||||||
|
|
||||||
|
**3. Application of GNU Kind Communication Policy**
|
||||||
|
|
||||||
|
These terms also align with the principles outlined in the [GNU Kind Communication Policy](https://www.gnu.org/philosophy/kind-communication.html), which encourage respectful and inclusive communication within the Poketube community.
|
||||||
|
|
||||||
|
It is crucial to note that we respect the diverse opinions and beliefs of our users.
|
||||||
|
|
||||||
|
***Additional terms end lol***
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
|
|
34
Dockerfile
Normal file
34
Dockerfile
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
## To build the image, run:
|
||||||
|
## docker build -t poketube .
|
||||||
|
|
||||||
|
## To run the image, run:
|
||||||
|
## docker run -d -p 6003:6003 poketube
|
||||||
|
|
||||||
|
# Base (Debian)
|
||||||
|
FROM debian
|
||||||
|
|
||||||
|
# Set Work Directory
|
||||||
|
WORKDIR /poketube
|
||||||
|
COPY . /poketube
|
||||||
|
|
||||||
|
# Expose Ports
|
||||||
|
EXPOSE 6003
|
||||||
|
|
||||||
|
# Install Requirements
|
||||||
|
RUN apt-get update && apt-get -y install \
|
||||||
|
libcurl4-openssl-dev make g++ ca-certificates curl gnupg
|
||||||
|
|
||||||
|
# Install NodeJS v18
|
||||||
|
RUN mkdir -p /etc/apt/keyrings
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||||
|
|
||||||
|
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get -y install nodejs npm
|
||||||
|
|
||||||
|
# Install Packages
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Run
|
||||||
|
CMD npm start
|
7
HOSTING_REQUIREMENTS.md
Normal file
7
HOSTING_REQUIREMENTS.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Internet: 1GBIT (recommended 5GBIT)
|
||||||
|
|
||||||
|
RAM: 2GB
|
||||||
|
|
||||||
|
CPU: Any recent one
|
||||||
|
|
||||||
|
OS: Microsoft Windows, macOS or GNU/LINUX (doesnt work on "musl" distros ) with systemd or any service manager
|
13
LICENSE-APPSTORE.md
Normal file
13
LICENSE-APPSTORE.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
The developers are aware that the terms of service that apply to apps distributed via Apple's App Store services and similar app stores may conflict
|
||||||
|
with rights granted under the Poke license, the GNU General
|
||||||
|
Public License, version 3.
|
||||||
|
|
||||||
|
The copyright holders of the Poke project do not wish this conflict to prevent the otherwise-compliant distribution of derived apps via the App Store and similar app stores.
|
||||||
|
|
||||||
|
Therefore, we have committed not to pursue any license
|
||||||
|
violation that results solely from the conflict between the GNU GPLv3
|
||||||
|
and the Apple App Store terms of service or similar app stores. In
|
||||||
|
other words, as long as you comply with the GPL in all other respects,
|
||||||
|
including its requirements to provide users with source code and the
|
||||||
|
text of the license, we will not object to your distribution of the
|
||||||
|
Poke project through the App Store.
|
111
README.md
111
README.md
@ -1,23 +1,108 @@
|
|||||||
[](https://vshymanskyy.github.io/StandWithUkraine/)
|
<h1 align="center">
|
||||||
|
<a href="https://poketube.fun/watch?v=9sJUDx7iEJw&quality=medium&=sjohgteojgytrueugtye4jhtytjrjnyıı">
|
||||||
|
<img src="https://poketube.fun/css/logo-poke.svg" width="400">
|
||||||
|
</a>
|
||||||
|
<a href="http://www.defectivebydesign.org/drm-free">
|
||||||
|
<img src="https://static.fsf.org/dbd/label/DRM-free%20label%20120.en.png"
|
||||||
|
alt="DRM Free" width="65" height="65" border="0" align="middle" />
|
||||||
|
</a>
|
||||||
|
<p>THE PRIVACY APP OF YOUR DREAMS! :3</p>
|
||||||
|
</h1>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://poketube.fun/watch?v=dQw4w9WgXcQ">
|
<p>Be anonymous while watching (cat falling) videos, searching the web, and listening to music on Poke - the free privacy front-end!</p>
|
||||||
<img src="https://user-images.githubusercontent.com/65588168/156941082-2fe10d35-bc1d-4928-9c5d-32d91cdea3d8.png">
|
|
||||||
</a>
|
|
||||||
<h1> POKETUBE </h1>
|
|
||||||
youtube player that loves privacy
|
|
||||||
<br>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="#welcome">Welcome!</a> | <a href="#features">Features</a> | <a href="#no-non-free-codec-needed">No Non-Free Codec</a> | <a href="#hosting-poke">Hosting</a> | <a href="#poke-community">Community</a> | <a href="#legal">Legal</a>
|
||||||
|
<br><br>
|
||||||
|
<img src="https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg" alt="Stand with Ukraine">
|
||||||
|
<a href="./LICENSE">
|
||||||
|
<img src="https://img.shields.io/badge/License-GPL--3-FF6666" alt="GPL-3-or-later License">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Welcome!
|
||||||
|
|
||||||
|
Welcome to Poke (formerly PokeTube), the privacy-friendly YouTube front-end built with the invidious API! Imagine paying for YouTube Premium just to download videos - couldn't be us (literally).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Works on Older browsers!
|
| <img width="100%" style="border-radius: 24px" src="./css/README_RYD.png"> | <div style="text-align: left"><h3>🔙 Built-In Return YouTube Dislikes</h3>See the dislikes from *returnyoutubedislike* - because sometimes you need to know how bad that video really is :3</div> |
|
||||||
- Javascript-free on frontend
|
| - | - |
|
||||||
- No advetisment
|
| <div style="text-align: right"><h3>📱 PWA Support</h3>With PWA Support, you can install Poke on your mobile device. Now you can pretend to be productive while watching cat videos on the go, mreoww! :3</div> | <img width="100%" style="border-radius: 24px" src="./css/README_PWA.jpg"> |
|
||||||
- See Dislike counts from [Return YouTube Dislike Api](https://www.returnyoutubedislike.com/) (see:[Line 22 of fetcher.js](https://github.com/iamashley0/poketube/blob/main/src/fetcher.js#L22))
|
| <h3>🎨 Customize</h3>Customize Poke however you want. Make it as unique as your taste in memes. | <h3>📥 Accounts</h3>Suscribe (yes Suscribe hehe sussy baka) to whaever channel you want! </div> |
|
||||||
- Uses LightTube api
|
| <h3>🔍 Web Search</h3>Search the web privately on PokeTube. Incognito mode who? | <h3>🎶 And...</h3>Ambient mode, HQ audio, and even more! :3 |
|
||||||
|
|
||||||
|
## No Non-Free Codec Needed
|
||||||
|
|
||||||
|
Poke uses OpenH264, which is free software! No non-free components included :3 View the source code of OpenH264 [here](https://github.com/cisco/openh264.git). Because who wants to deal with non-free stuff? Not us!~
|
||||||
|
|
||||||
|
## Hosting Poke
|
||||||
|
|
||||||
|
### NodeJS
|
||||||
|
|
||||||
|
1. **Install Packages**
|
||||||
|
- Fedora/RHEL GNU/linux: `$ sudo dnf install git make gcc libcurl nodejs python libcurl g++ curl-config`
|
||||||
|
- Debian/Ubuntu GNU/linux: `$ sudo apt install git make gcc libcurl4-openssl-dev nodejs npm python g++`
|
||||||
|
- Alpine Linux (non-gnu): `$ apk add git nodejs npm python make gcc g++ libcurl curl-dev`
|
||||||
|
|
||||||
|
(we dont know if it works on non-gnu systems)
|
||||||
|
|
||||||
|
2. **Clone Repo**
|
||||||
|
- Codeberg: `$ git clone https://codeberg.org/ashley/poke.git`
|
||||||
|
- GitHub: `$ git clone https://github.com/ashley0143/poke.git`
|
||||||
|
|
||||||
|
reccomended unoffical mirrors:
|
||||||
|
- code.lgbt: `$ git clone https://code.lgbt/mirror/poke.git` [sync every 10mins]
|
||||||
|
- git.lgbt: `$ git clone https://git.lgbt/mirror/poke.git` [sync every 10mins]
|
||||||
|
- nin0git :`$ git clone https://git.nin0.dev/mirrors/poke.git` [sync every 10mins]
|
||||||
|
|
||||||
|
not reccomended, unstable
|
||||||
|
- SudoVanilla: `$ git clone https://ark.sudovanilla.org/Korbs/poke.git` [sync every week]
|
||||||
|
|
||||||
|
3. **Install Dependencies**
|
||||||
|
- `$ cd poke`
|
||||||
|
- `$ npm install`
|
||||||
|
|
||||||
|
4. **Start Server**
|
||||||
|
- `$ node server.js`
|
||||||
|
|
||||||
|
Congrats! Poke should now be running on `localhost:6003`! 🎉
|
||||||
|
|
||||||
|
## Poke Community
|
||||||
|
|
||||||
|
Join us on [Discord](https://discord.poketube.fun/) or [Matrix](https://matrix.to/#/#poke:vern.cc)! I promise we're cool! <3
|
||||||
|
|
||||||
|
or if u like fedi, we host [PokeSocial](https://social.poketube.fun) as well :3
|
||||||
|
|
||||||
|
## The Legal Stuff (boring tbh)
|
||||||
|
the main parts of the project is Under GPL-3.0-OR-LATER :3
|
||||||
|
|
||||||
|
see the each sections LICENSE tho!!
|
||||||
|
|
||||||
|
Copyleft 2021-202x Poke Project, mostly ashley0143 - poke does not support the ["source first"](https://sourcefirst.com/) or ["open source"](https://opensource.org) movement :3 - we support the free software movement (fsf.org)
|
||||||
|
|
||||||
|
|
||||||
tbanks to <a href="https://gitlab.com/kuylar/lighttube">LIGHTTUBE</a>
|
[Code Of conduct](https://codeberg.org/Ashley/poke/src/branch/main/CODE_OF_CONDUCT.md)
|
||||||
|
|
||||||
|
[Privacy Policy](https://poketube.fun/privacy)
|
||||||
|
|
||||||
|
TL;DR: we dont collect or share your personal info, that's it lol.
|
||||||
|
|
||||||
|
We use the GNU Coding Standard Thingy, see [this link.](https://www.gnu.org/prep/standards)
|
||||||
|
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<p>Parts of Poke are hosted on Glitch.com since <i>2020</i>.</p>
|
||||||
|
<a href="https://glitch.com/">
|
||||||
|
<img src="https://cdn.glitch.global/d68d17bb-f2c0-4bc3-993f-50902734f652/glitch-fastly-lock-up.svg">
|
||||||
|
</a>
|
||||||
|
<br><hr>
|
||||||
|
<p> this software does not support <a href="https://opensource.org">"OSI"</a> - <a href="https://rationalwiki.org/wiki/Eric_S._Raymond#Open_Source">they are terrible people </a> - pls support the FSF/GNU (<a href="https://fsf.org">fsf.org</a> or <a href="https://gnu.org">gnu.org</a>) instead :3 </p>
|
||||||
|
<a href="https://gnu.org/not-open-source">
|
||||||
|
<img width="200" src="https://autumn.revolt.chat/attachments/eNpfwV2C1_wudONe43YCvWr-4vbvLpG78HbuXgOYfO">
|
||||||
|
</a>
|
||||||
|
</div>
|
241
alac/ALACMagicCookieDescription.txt
Normal file
241
alac/ALACMagicCookieDescription.txt
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
Apple Lossless Format "Magic Cookie" Description
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
|
||||||
|
Many encoded formats for audio require additional, codec specific configuration information in order to operate successfully.
|
||||||
|
This codec specific information is often called a 'magic cookie'. The Apple Lossless codec's 'magic cookie' contains the
|
||||||
|
ALACSpecificConfig and optional ALACChannelLayoutInfo (both described below).
|
||||||
|
|
||||||
|
The 'magic cookie' must accompany the bitstream when stored in any file container (M4A/MP4, CAF) so that it may be provided to the
|
||||||
|
decoder when decoding the bitstream. From the caller's perspective, the 'magic cookie' is opaque and should be stored in the file
|
||||||
|
and presented to the decoder exactly as it is vended from the encoder (and consequently stored in the file).
|
||||||
|
|
||||||
|
The ALAC 'magic cookie' as stored in a file has all fields described in big-endian order (regardless of file format).
|
||||||
|
|
||||||
|
The layout of the 'magic cookie' is as follows:
|
||||||
|
|
||||||
|
|
||||||
|
---------------- ALAC Specific Info (24 bytes) (mandatory) ---------------------------
|
||||||
|
(ALACSpecificConfig) Decoder Info
|
||||||
|
|
||||||
|
---------------- Channel Layout Info (24 bytes) (optional) ----------------------------
|
||||||
|
(ALAC Channel Layout Info) Channel Layout Info
|
||||||
|
|
||||||
|
|
||||||
|
If the channel layout is absent from the cookie, then the following assumptions are made:
|
||||||
|
1 channel - mono
|
||||||
|
2 channels - stereo in left, right order
|
||||||
|
> 2 channels - no specific channel designation or role.
|
||||||
|
|
||||||
|
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
* ALAC Specific Info (24 bytes) (mandatory)
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
|
||||||
|
The Apple Lossless codec stores specific information about the encoded stream in the ALACSpecificConfig. This
|
||||||
|
info is vended by the encoder and is used to setup the decoder for a given encoded bitstream.
|
||||||
|
|
||||||
|
When read from and written to a file, the fields of this struct must be in big-endian order.
|
||||||
|
When vended by the encoder (and received by the decoder) the struct values will be in big-endian order.
|
||||||
|
|
||||||
|
/*
|
||||||
|
struct ALACSpecificConfig (defined in ALACAudioTypes.h)
|
||||||
|
abstract This struct is used to describe codec provided information about the encoded Apple Lossless bitstream.
|
||||||
|
It must accompany the encoded stream in the containing audio file and be provided to the decoder.
|
||||||
|
|
||||||
|
field frameLength uint32_t indicating the frames per packet when no explicit frames per packet setting is
|
||||||
|
present in the packet header. The encoder frames per packet can be explicitly set
|
||||||
|
but for maximum compatibility, the default encoder setting of 4096 should be used.
|
||||||
|
|
||||||
|
field compatibleVersion uint8_t indicating compatible version,
|
||||||
|
value must be set to 0
|
||||||
|
|
||||||
|
field bitDepth uint8_t describes the bit depth of the source PCM data (maximum value = 32)
|
||||||
|
|
||||||
|
field pb uint8_t currently unused tuning parameter.
|
||||||
|
value should be set to 40
|
||||||
|
|
||||||
|
field mb uint8_t currently unused tuning parameter.
|
||||||
|
value should be set to 10
|
||||||
|
|
||||||
|
field kb uint8_t currently unused tuning parameter.
|
||||||
|
value should be set to 14
|
||||||
|
|
||||||
|
field numChannels uint8_t describes the channel count (1 = mono, 2 = stereo, etc...)
|
||||||
|
when channel layout info is not provided in the 'magic cookie', a channel count > 2
|
||||||
|
describes a set of discreet channels with no specific ordering
|
||||||
|
|
||||||
|
field maxRun uint16_t currently unused.
|
||||||
|
value should be set to 255
|
||||||
|
|
||||||
|
field maxFrameBytes uint32_t the maximum size of an Apple Lossless packet within the encoded stream.
|
||||||
|
value of 0 indicates unknown
|
||||||
|
|
||||||
|
field avgBitRate uint32_t the average bit rate in bits per second of the Apple Lossless stream.
|
||||||
|
value of 0 indicates unknown
|
||||||
|
|
||||||
|
field sampleRate uint32_t sample rate of the encoded stream
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef struct ALACSpecificConfig
|
||||||
|
{
|
||||||
|
uint32_t frameLength;
|
||||||
|
uint8_t compatibleVersion;
|
||||||
|
uint8_t bitDepth;
|
||||||
|
uint8_t pb;
|
||||||
|
uint8_t mb;
|
||||||
|
uint8_t kb;
|
||||||
|
uint8_t numChannels;
|
||||||
|
uint16_t maxRun;
|
||||||
|
uint32_t maxFrameBytes;
|
||||||
|
uint32_t avgBitRate;
|
||||||
|
uint32_t sampleRate;
|
||||||
|
|
||||||
|
} ALACSpecificConfig;
|
||||||
|
|
||||||
|
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
Channel Layout Info (24 bytes) (optional)
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
|
||||||
|
The Apple Lossless codec can support a specific set of channel layouts. When channel information is vended
|
||||||
|
by the encoder (in the 'magic cookie'), it is formatted in the the ALACChannelLayoutInfo.
|
||||||
|
|
||||||
|
When read from and written to a file, the fields of this struct must be in big-endian order.
|
||||||
|
When vended by the encoder (and received by the decoder) the struct values will be in big-endian order.
|
||||||
|
|
||||||
|
/*
|
||||||
|
struct ALACChannelLayoutInfo (defined in ALACAudioTypes.h)
|
||||||
|
abstract This struct is used to specify particular channel orderings or configurations.
|
||||||
|
It is an optional portion of the 'magic cookie', being required to describe specific channel layouts (see below)
|
||||||
|
of more than 2 channels.
|
||||||
|
|
||||||
|
field channelLayoutInfoSize uint32_t indicates the size of the channel layout data
|
||||||
|
value should be set to 24
|
||||||
|
|
||||||
|
field channelLayoutInfoID uint32_t identifier indicating that channel layout info is present
|
||||||
|
value = 'chan'
|
||||||
|
|
||||||
|
field versionFlags uint32_t version flags
|
||||||
|
value should be set to 0
|
||||||
|
|
||||||
|
field channelLayoutTag uint32_t channel layout type
|
||||||
|
from defined list in ALACAudioTypes.h (see below)
|
||||||
|
|
||||||
|
field reserved1 uint32_t currently unused field
|
||||||
|
value should be set to 0
|
||||||
|
|
||||||
|
field reserved2 uint32_t currently unused field
|
||||||
|
value should be set to 0
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef struct ALACChannelLayoutInfo
|
||||||
|
{
|
||||||
|
uint32_t channelLayoutInfoSize;
|
||||||
|
uint32_t channelLayoutInfoID;
|
||||||
|
uint32_t versionFlags;
|
||||||
|
uint32_t channelLayoutTag;
|
||||||
|
uint32_t reserved1;
|
||||||
|
uint32_t reserved2;
|
||||||
|
} ALACChannelLayoutInfo;
|
||||||
|
|
||||||
|
|
||||||
|
* Channel Layout Tags
|
||||||
|
|
||||||
|
These constants will be used to describe the bitstream's channel layout. (defined in ALACAudioTypes.h)
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
kALACChannelLayoutTag_Mono = (100<<16) | 1, // C
|
||||||
|
kALACChannelLayoutTag_Stereo = (101<<16) | 2, // L R
|
||||||
|
kALACChannelLayoutTag_MPEG_3_0_B = (113<<16) | 3, // C L R
|
||||||
|
kALACChannelLayoutTag_MPEG_4_0_B = (116<<16) | 4, // C L R Cs
|
||||||
|
kALACChannelLayoutTag_MPEG_5_0_D = (120<<16) | 5, // C L R Ls Rs
|
||||||
|
kALACChannelLayoutTag_MPEG_5_1_D = (124<<16) | 6, // C L R Ls Rs LFE
|
||||||
|
kALACChannelLayoutTag_AAC_6_1 = (142<<16) | 7, // C L R Ls Rs Cs LFE
|
||||||
|
kALACChannelLayoutTag_MPEG_7_1_B = (127<<16) | 8 // C Lc Rc L R Ls Rs LFE (doc: IS-13818-7 MPEG2-AAC)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
* Storing Apple Lossless Magic Cookie in Audio Files
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
|
||||||
|
The Apple Lossless Magic Cookie is treated as opaque by file parsing code. The 'magic cookie' vended by the encoder
|
||||||
|
is placed without modification into the audio file and the read from that file and passed (unmodified) to the decoder.
|
||||||
|
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
* CAF File
|
||||||
|
|
||||||
|
In a CAF file (Core Audio File), the 'magic cookie' is stored in CAF's Magic Cookie chunk ('kuki').
|
||||||
|
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
* MP4/M4A File
|
||||||
|
|
||||||
|
In an MP4/M4A file, the 'magic cookie' is encapsulated in the AudioSample entry of a Sound Description box ('stsd').
|
||||||
|
An ISO style full box header to describe the ALACSpecificConfig portion is appended to the AudioSampleEntry, followed by the
|
||||||
|
'magic cookie' as it is vended by the encoder.
|
||||||
|
|
||||||
|
(All fields are stored in big-endian order: see ISO/IEC 14496-12 for a full description of the SoundDescription and AudioSampleEntry boxes, etc.)
|
||||||
|
|
||||||
|
---------------- SoundDescriptionBox (FullBox) ----------------------------
|
||||||
|
|
||||||
|
SampleEntrySize // = sizeof(SoundDescriptionBox)(16) + sizeof (AudioSampleEntry)(AudioSampleEntry.SampleEntrySize)
|
||||||
|
SampleEntryType // = 'stsd'
|
||||||
|
VersionFlags // = 0
|
||||||
|
EntryCount // = 1
|
||||||
|
|
||||||
|
---------------- Audio Sample Entry (REQUIRED) -----------------------------
|
||||||
|
|
||||||
|
SampleEntrySize // sizeof(AudioSampleEntry)(36) + sizeof(full ISO box header)(12) + sizeof(Apple Lossless Magic Cookie)
|
||||||
|
SampleEntryType // = 'alac', specifies that the AudioSampleEntry describes an Apple Lossless bitstream
|
||||||
|
mReserved[6] // = 0
|
||||||
|
dref index // = 1
|
||||||
|
reserved[2] // = 0
|
||||||
|
channel count // = number of channels as a uint_16 value
|
||||||
|
sample size // = source pcm bitdepth (example = 16bit source pcm)
|
||||||
|
predefined // = 0
|
||||||
|
reserved // = 0
|
||||||
|
sample rate // sample rate as a uint_32 value
|
||||||
|
|
||||||
|
Appended to AudioSampleEntry:
|
||||||
|
|
||||||
|
ALAC Specific Info Size // uint_32 value, = 36 (12 + sizeof(ALACSpecificConfig))
|
||||||
|
ALAC Specific Info ID // uint_32 value, = 'alac', format ID which matches the Audio Sample Entry SampleEntryType field
|
||||||
|
Version Flags // uint_32 value, = 0
|
||||||
|
|
||||||
|
Apple Lossless Magic Cookie // 'magic cookie' vended from ALAC encoder (24 or 48 Bytes)
|
||||||
|
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
* Compatibility
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
__________________________________________________________________________________________________________________________________
|
||||||
|
|
||||||
|
Previous versions of the Apple Lossless encoder vended a different 'magic cookie'. To ensure compatibility, the Apple Lossless decoder
|
||||||
|
must be prepared to parse a 'magic cookie' in the format described below. Note that the 'magic cookie' defined above is
|
||||||
|
encapsulated in the following method and can be extracted as a contiguous set of bytes.
|
||||||
|
|
||||||
|
|
||||||
|
---------------- Format Atom (12 bytes) --------------------------------
|
||||||
|
(uint_32) Format Atom Size // = 12
|
||||||
|
(uint_32) Channel Layout Info ID // = 'frma'
|
||||||
|
(uint_32) Format Type // = 'alac'
|
||||||
|
|
||||||
|
---------------- ALAC Specific Info (36 bytes) (required) --------------
|
||||||
|
(uint_32) ALAC Specific Info Size // = 36 (12 + sizeof(ALACSpecificConfig))
|
||||||
|
(uint_32) ALAC Specific Info ID // = 'alac', format ID which matches the Audio Sample Entry SampleEntryType field
|
||||||
|
(uint_32) Version Flags // = 0
|
||||||
|
|
||||||
|
[ Apple Lossless Magic Cookie (see above) ]
|
||||||
|
|
||||||
|
---------------- Terminator Atom (8 bytes) -----------------------------
|
||||||
|
(uint_32) Channel Layout Info Size // = 8
|
||||||
|
(uint_32) Channel Layout Info ID // = 0
|
||||||
|
|
||||||
|
|
||||||
|
|
53
alac/LICENSE
Normal file
53
alac/LICENSE
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
Apache License
|
||||||
|
|
||||||
|
Version 2.0, January 2004
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||||
|
|
||||||
|
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||||
|
|
||||||
|
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||||
|
|
||||||
|
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
44
alac/ReadMe.txt
Normal file
44
alac/ReadMe.txt
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
The Apple Lossless Format
|
||||||
|
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
Apple Lossless supports the following features. Not all of these are implemented in alacconvert, though they are in the codec code provided.
|
||||||
|
|
||||||
|
1. Bit depths 16, 20, 24 and 32 bits.
|
||||||
|
2. Any arbitrary integer sample rate from 1 to 384,000 Hz. In theory rates up to 4,294,967,295 (2^32 - 1) Hz could be supported.
|
||||||
|
3. From one to eight channels are supported. Channel orders for the supported formats are described as:
|
||||||
|
Num Chan Order
|
||||||
|
1 mono
|
||||||
|
2 stereo (Left, Right)
|
||||||
|
3 MPEG 3.0 B (Center, Left, Right)
|
||||||
|
4 MPEG 4.0 B (Center, Left, Right, Center Surround)
|
||||||
|
5 MPEG 5.0 D (Center, Left, Right, Left Surround, Right Surround)
|
||||||
|
6 MPEG 5.1 D (Center, Left, Right, Left Surround, Right Surround, Low Frequency Effects)
|
||||||
|
7 Apple AAC 6.1 (Center, Left, Right, Left Surround, Right Surround, Center Surround, Low Frequency Effects)
|
||||||
|
8 MPEG 7.1 B (Center, Left Center, Right Center, Left, Right, Left Surround, Right Surround, Low Frequency Effects)
|
||||||
|
4. Packet size defaults to 4096 sample frames of audio per packet. Other packet sizes are certainly possible. However, non-default packet sizes are not guaranteed to work properly on all hardware devices that support Apple Lossless. Packets above 16,384 sample frames are not supported.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
This package contains the sources for the Apple Lossless (ALAC) encoder and decoder.
|
||||||
|
|
||||||
|
The "codec" directory contains all the sources necessary for a functioning codec. Also includes is a makefile that will build libalac.a on a UNIX/Linux machine.
|
||||||
|
|
||||||
|
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
ALACconvert
|
||||||
|
|
||||||
|
The convert-utility directory contains sources to build alacconvert which is a simple utility that demonstrates how to use the included ALAC encoder and decoder.
|
||||||
|
|
||||||
|
alacconvert supports the following formats:
|
||||||
|
|
||||||
|
1. 16- or 24-bit mono or stereo .wav files where the data is little endian integer. Extended WAVE format chunks are not handled.
|
||||||
|
2. 16- or 24-bit mono or stereo .caf (Core Audio Format) files as well as certain multi-channel configurations where the data is big or little endian integer. It does no channel order manipulation.
|
||||||
|
3. ALAC .caf files.
|
||||||
|
|
||||||
|
Three project are provided to build a command line utility called alacconvert that converts cpm data to ALAC or vice versa. A Mac OS X Xcode project, A Windows Visual Studio project, and a generic UNIX/Linux make file.
|
||||||
|
|
||||||
|
Note: When building on Windows, if you are using a version of Visual Studio before Visual Studio 2010, <stdint.h> is not installed. You will need to acquire this file on your own. It can be put in the same directory as the project.
|
||||||
|
|
||||||
|
|
||||||
|
|
197
alac/codec/ALACAudioTypes.h
Normal file
197
alac/codec/ALACAudioTypes.h
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
File: ALACAudioTypes.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef ALACAUDIOTYPES_H
|
||||||
|
#define ALACAUDIOTYPES_H
|
||||||
|
|
||||||
|
#if PRAGMA_ONCE
|
||||||
|
#pragma once
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if PRAGMA_STRUCT_ALIGN
|
||||||
|
#pragma options align=mac68k
|
||||||
|
#elif PRAGMA_STRUCT_PACKPUSH
|
||||||
|
#pragma pack(push, 2)
|
||||||
|
#elif PRAGMA_STRUCT_PACK
|
||||||
|
#pragma pack(2)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#if defined(__ppc__)
|
||||||
|
#define TARGET_RT_BIG_ENDIAN 1
|
||||||
|
#elif defined(__ppc64__)
|
||||||
|
#define TARGET_RT_BIG_ENDIAN 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define kChannelAtomSize 12
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
kALAC_UnimplementedError = -4,
|
||||||
|
kALAC_FileNotFoundError = -43,
|
||||||
|
kALAC_ParamError = -50,
|
||||||
|
kALAC_MemFullError = -108
|
||||||
|
};
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
kALACFormatAppleLossless = 'alac',
|
||||||
|
kALACFormatLinearPCM = 'lpcm'
|
||||||
|
};
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
kALACMaxChannels = 8,
|
||||||
|
kALACMaxEscapeHeaderBytes = 8,
|
||||||
|
kALACMaxSearches = 16,
|
||||||
|
kALACMaxCoefs = 16,
|
||||||
|
kALACDefaultFramesPerPacket = 4096
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef uint32_t ALACChannelLayoutTag;
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
kALACFormatFlagIsFloat = (1 << 0), // 0x1
|
||||||
|
kALACFormatFlagIsBigEndian = (1 << 1), // 0x2
|
||||||
|
kALACFormatFlagIsSignedInteger = (1 << 2), // 0x4
|
||||||
|
kALACFormatFlagIsPacked = (1 << 3), // 0x8
|
||||||
|
kALACFormatFlagIsAlignedHigh = (1 << 4), // 0x10
|
||||||
|
};
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
#if TARGET_RT_BIG_ENDIAN
|
||||||
|
kALACFormatFlagsNativeEndian = kALACFormatFlagIsBigEndian
|
||||||
|
#else
|
||||||
|
kALACFormatFlagsNativeEndian = 0
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
// this is required to be an IEEE 64bit float
|
||||||
|
typedef double alac_float64_t;
|
||||||
|
|
||||||
|
// These are the Channel Layout Tags used in the Channel Layout Info portion of the ALAC magic cookie
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
kALACChannelLayoutTag_Mono = (100<<16) | 1, // C
|
||||||
|
kALACChannelLayoutTag_Stereo = (101<<16) | 2, // L R
|
||||||
|
kALACChannelLayoutTag_MPEG_3_0_B = (113<<16) | 3, // C L R
|
||||||
|
kALACChannelLayoutTag_MPEG_4_0_B = (116<<16) | 4, // C L R Cs
|
||||||
|
kALACChannelLayoutTag_MPEG_5_0_D = (120<<16) | 5, // C L R Ls Rs
|
||||||
|
kALACChannelLayoutTag_MPEG_5_1_D = (124<<16) | 6, // C L R Ls Rs LFE
|
||||||
|
kALACChannelLayoutTag_AAC_6_1 = (142<<16) | 7, // C L R Ls Rs Cs LFE
|
||||||
|
kALACChannelLayoutTag_MPEG_7_1_B = (127<<16) | 8 // C Lc Rc L R Ls Rs LFE (doc: IS-13818-7 MPEG2-AAC)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ALAC currently only utilizes these channels layouts. There is a one for one correspondance between a
|
||||||
|
// given number of channels and one of these layout tags
|
||||||
|
static const ALACChannelLayoutTag ALACChannelLayoutTags[kALACMaxChannels] =
|
||||||
|
{
|
||||||
|
kALACChannelLayoutTag_Mono, // C
|
||||||
|
kALACChannelLayoutTag_Stereo, // L R
|
||||||
|
kALACChannelLayoutTag_MPEG_3_0_B, // C L R
|
||||||
|
kALACChannelLayoutTag_MPEG_4_0_B, // C L R Cs
|
||||||
|
kALACChannelLayoutTag_MPEG_5_0_D, // C L R Ls Rs
|
||||||
|
kALACChannelLayoutTag_MPEG_5_1_D, // C L R Ls Rs LFE
|
||||||
|
kALACChannelLayoutTag_AAC_6_1, // C L R Ls Rs Cs LFE
|
||||||
|
kALACChannelLayoutTag_MPEG_7_1_B // C Lc Rc L R Ls Rs LFE (doc: IS-13818-7 MPEG2-AAC)
|
||||||
|
};
|
||||||
|
|
||||||
|
// AudioChannelLayout from CoreAudioTypes.h. We never need the AudioChannelDescription so we remove it
|
||||||
|
struct ALACAudioChannelLayout
|
||||||
|
{
|
||||||
|
ALACChannelLayoutTag mChannelLayoutTag;
|
||||||
|
uint32_t mChannelBitmap;
|
||||||
|
uint32_t mNumberChannelDescriptions;
|
||||||
|
};
|
||||||
|
typedef struct ALACAudioChannelLayout ALACAudioChannelLayout;
|
||||||
|
|
||||||
|
struct AudioFormatDescription
|
||||||
|
{
|
||||||
|
alac_float64_t mSampleRate;
|
||||||
|
uint32_t mFormatID;
|
||||||
|
uint32_t mFormatFlags;
|
||||||
|
uint32_t mBytesPerPacket;
|
||||||
|
uint32_t mFramesPerPacket;
|
||||||
|
uint32_t mBytesPerFrame;
|
||||||
|
uint32_t mChannelsPerFrame;
|
||||||
|
uint32_t mBitsPerChannel;
|
||||||
|
uint32_t mReserved;
|
||||||
|
};
|
||||||
|
typedef struct AudioFormatDescription AudioFormatDescription;
|
||||||
|
|
||||||
|
/* Lossless Definitions */
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
kALACCodecFormat = 'alac',
|
||||||
|
kALACVersion = 0,
|
||||||
|
kALACCompatibleVersion = kALACVersion,
|
||||||
|
kALACDefaultFrameSize = 4096
|
||||||
|
};
|
||||||
|
|
||||||
|
// note: this struct is wrapped in an 'alac' atom in the sample description extension area
|
||||||
|
// note: in QT movies, it will be further wrapped in a 'wave' atom surrounded by 'frma' and 'term' atoms
|
||||||
|
typedef struct ALACSpecificConfig
|
||||||
|
{
|
||||||
|
uint32_t frameLength;
|
||||||
|
uint8_t compatibleVersion;
|
||||||
|
uint8_t bitDepth; // max 32
|
||||||
|
uint8_t pb; // 0 <= pb <= 255
|
||||||
|
uint8_t mb;
|
||||||
|
uint8_t kb;
|
||||||
|
uint8_t numChannels;
|
||||||
|
uint16_t maxRun;
|
||||||
|
uint32_t maxFrameBytes;
|
||||||
|
uint32_t avgBitRate;
|
||||||
|
uint32_t sampleRate;
|
||||||
|
|
||||||
|
} ALACSpecificConfig;
|
||||||
|
|
||||||
|
|
||||||
|
// The AudioChannelLayout atom type is not exposed yet so define it here
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
AudioChannelLayoutAID = 'chan'
|
||||||
|
};
|
||||||
|
|
||||||
|
#if PRAGMA_STRUCT_ALIGN
|
||||||
|
#pragma options align=reset
|
||||||
|
#elif PRAGMA_STRUCT_PACKPUSH
|
||||||
|
#pragma pack(pop)
|
||||||
|
#elif PRAGMA_STRUCT_PACK
|
||||||
|
#pragma pack()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* ALACAUDIOTYPES_H */
|
260
alac/codec/ALACBitUtilities.c
Normal file
260
alac/codec/ALACBitUtilities.c
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*=============================================================================
|
||||||
|
File: ALACBitUtilities.c
|
||||||
|
|
||||||
|
$NoKeywords: $
|
||||||
|
=============================================================================*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "ALACBitUtilities.h"
|
||||||
|
|
||||||
|
// BitBufferInit
|
||||||
|
//
|
||||||
|
void BitBufferInit( BitBuffer * bits, uint8_t * buffer, uint32_t byteSize )
|
||||||
|
{
|
||||||
|
bits->cur = buffer;
|
||||||
|
bits->end = bits->cur + byteSize;
|
||||||
|
bits->bitIndex = 0;
|
||||||
|
bits->byteSize = byteSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitBufferRead
|
||||||
|
//
|
||||||
|
uint32_t BitBufferRead( BitBuffer * bits, uint8_t numBits )
|
||||||
|
{
|
||||||
|
uint32_t returnBits;
|
||||||
|
|
||||||
|
//Assert( numBits <= 16 );
|
||||||
|
|
||||||
|
returnBits = ((uint32_t)bits->cur[0] << 16) | ((uint32_t)bits->cur[1] << 8) | ((uint32_t)bits->cur[2]);
|
||||||
|
returnBits = returnBits << bits->bitIndex;
|
||||||
|
returnBits &= 0x00FFFFFF;
|
||||||
|
|
||||||
|
bits->bitIndex += numBits;
|
||||||
|
|
||||||
|
returnBits = returnBits >> (24 - numBits);
|
||||||
|
|
||||||
|
bits->cur += (bits->bitIndex >> 3);
|
||||||
|
bits->bitIndex &= 7;
|
||||||
|
|
||||||
|
//Assert( bits->cur <= bits->end );
|
||||||
|
|
||||||
|
return returnBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitBufferReadSmall
|
||||||
|
//
|
||||||
|
// Reads up to 8 bits
|
||||||
|
uint8_t BitBufferReadSmall( BitBuffer * bits, uint8_t numBits )
|
||||||
|
{
|
||||||
|
uint16_t returnBits;
|
||||||
|
|
||||||
|
//Assert( numBits <= 8 );
|
||||||
|
|
||||||
|
returnBits = (bits->cur[0] << 8) | bits->cur[1];
|
||||||
|
returnBits = returnBits << bits->bitIndex;
|
||||||
|
|
||||||
|
bits->bitIndex += numBits;
|
||||||
|
|
||||||
|
returnBits = returnBits >> (16 - numBits);
|
||||||
|
|
||||||
|
bits->cur += (bits->bitIndex >> 3);
|
||||||
|
bits->bitIndex &= 7;
|
||||||
|
|
||||||
|
//Assert( bits->cur <= bits->end );
|
||||||
|
|
||||||
|
return (uint8_t)returnBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitBufferReadOne
|
||||||
|
//
|
||||||
|
// Reads one byte
|
||||||
|
uint8_t BitBufferReadOne( BitBuffer * bits )
|
||||||
|
{
|
||||||
|
uint8_t returnBits;
|
||||||
|
|
||||||
|
returnBits = (bits->cur[0] >> (7 - bits->bitIndex)) & 1;
|
||||||
|
|
||||||
|
bits->bitIndex++;
|
||||||
|
|
||||||
|
bits->cur += (bits->bitIndex >> 3);
|
||||||
|
bits->bitIndex &= 7;
|
||||||
|
|
||||||
|
//Assert( bits->cur <= bits->end );
|
||||||
|
|
||||||
|
return returnBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitBufferPeek
|
||||||
|
//
|
||||||
|
uint32_t BitBufferPeek( BitBuffer * bits, uint8_t numBits )
|
||||||
|
{
|
||||||
|
return ((((((uint32_t) bits->cur[0] << 16) | ((uint32_t) bits->cur[1] << 8) |
|
||||||
|
((uint32_t) bits->cur[2])) << bits->bitIndex) & 0x00FFFFFF) >> (24 - numBits));
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitBufferPeekOne
|
||||||
|
//
|
||||||
|
uint32_t BitBufferPeekOne( BitBuffer * bits )
|
||||||
|
{
|
||||||
|
return ((bits->cur[0] >> (7 - bits->bitIndex)) & 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitBufferUnpackBERSize
|
||||||
|
//
|
||||||
|
uint32_t BitBufferUnpackBERSize( BitBuffer * bits )
|
||||||
|
{
|
||||||
|
uint32_t size;
|
||||||
|
uint8_t tmp;
|
||||||
|
|
||||||
|
for ( size = 0, tmp = 0x80u; tmp &= 0x80u; size = (size << 7u) | (tmp & 0x7fu) )
|
||||||
|
tmp = (uint8_t) BitBufferReadSmall( bits, 8 );
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitBufferGetPosition
|
||||||
|
//
|
||||||
|
uint32_t BitBufferGetPosition( BitBuffer * bits )
|
||||||
|
{
|
||||||
|
uint8_t * begin;
|
||||||
|
|
||||||
|
begin = bits->end - bits->byteSize;
|
||||||
|
|
||||||
|
return ((uint32_t)(bits->cur - begin) * 8) + bits->bitIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitBufferByteAlign
|
||||||
|
//
|
||||||
|
void BitBufferByteAlign( BitBuffer * bits, int32_t addZeros )
|
||||||
|
{
|
||||||
|
// align bit buffer to next byte boundary, writing zeros if requested
|
||||||
|
if ( bits->bitIndex == 0 )
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ( addZeros )
|
||||||
|
BitBufferWrite( bits, 0, 8 - bits->bitIndex );
|
||||||
|
else
|
||||||
|
BitBufferAdvance( bits, 8 - bits->bitIndex );
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitBufferAdvance
|
||||||
|
//
|
||||||
|
void BitBufferAdvance( BitBuffer * bits, uint32_t numBits )
|
||||||
|
{
|
||||||
|
if ( numBits )
|
||||||
|
{
|
||||||
|
bits->bitIndex += numBits;
|
||||||
|
bits->cur += (bits->bitIndex >> 3);
|
||||||
|
bits->bitIndex &= 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitBufferRewind
|
||||||
|
//
|
||||||
|
void BitBufferRewind( BitBuffer * bits, uint32_t numBits )
|
||||||
|
{
|
||||||
|
uint32_t numBytes;
|
||||||
|
|
||||||
|
if ( numBits == 0 )
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ( bits->bitIndex >= numBits )
|
||||||
|
{
|
||||||
|
bits->bitIndex -= numBits;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
numBits -= bits->bitIndex;
|
||||||
|
bits->bitIndex = 0;
|
||||||
|
|
||||||
|
numBytes = numBits / 8;
|
||||||
|
numBits = numBits % 8;
|
||||||
|
|
||||||
|
bits->cur -= numBytes;
|
||||||
|
|
||||||
|
if ( numBits > 0 )
|
||||||
|
{
|
||||||
|
bits->bitIndex = 8 - numBits;
|
||||||
|
bits->cur--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( bits->cur < (bits->end - bits->byteSize) )
|
||||||
|
{
|
||||||
|
//DebugCMsg("BitBufferRewind: Rewound too far.");
|
||||||
|
|
||||||
|
bits->cur = (bits->end - bits->byteSize);
|
||||||
|
bits->bitIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitBufferWrite
|
||||||
|
//
|
||||||
|
void BitBufferWrite( BitBuffer * bits, uint32_t bitValues, uint32_t numBits )
|
||||||
|
{
|
||||||
|
uint32_t invBitIndex;
|
||||||
|
|
||||||
|
RequireAction( bits != nil, return; );
|
||||||
|
RequireActionSilent( numBits > 0, return; );
|
||||||
|
|
||||||
|
invBitIndex = 8 - bits->bitIndex;
|
||||||
|
|
||||||
|
while ( numBits > 0 )
|
||||||
|
{
|
||||||
|
uint32_t tmp;
|
||||||
|
uint8_t shift;
|
||||||
|
uint8_t mask;
|
||||||
|
uint32_t curNum;
|
||||||
|
|
||||||
|
curNum = MIN( invBitIndex, numBits );
|
||||||
|
|
||||||
|
tmp = bitValues >> (numBits - curNum);
|
||||||
|
|
||||||
|
shift = (uint8_t)(invBitIndex - curNum);
|
||||||
|
mask = 0xffu >> (8 - curNum); // must be done in two steps to avoid compiler sequencing ambiguity
|
||||||
|
mask <<= shift;
|
||||||
|
|
||||||
|
bits->cur[0] = (bits->cur[0] & ~mask) | (((uint8_t) tmp << shift) & mask);
|
||||||
|
numBits -= curNum;
|
||||||
|
|
||||||
|
// increment to next byte if need be
|
||||||
|
invBitIndex -= curNum;
|
||||||
|
if ( invBitIndex == 0 )
|
||||||
|
{
|
||||||
|
invBitIndex = 8;
|
||||||
|
bits->cur++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bits->bitIndex = 8 - invBitIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BitBufferReset( BitBuffer * bits )
|
||||||
|
//void BitBufferInit( BitBuffer * bits, uint8_t * buffer, uint32_t byteSize )
|
||||||
|
{
|
||||||
|
bits->cur = bits->end - bits->byteSize;
|
||||||
|
bits->bitIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if PRAGMA_MARK
|
||||||
|
#pragma mark -
|
||||||
|
#endif
|
104
alac/codec/ALACBitUtilities.h
Normal file
104
alac/codec/ALACBitUtilities.h
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*=============================================================================
|
||||||
|
File: ALACBitUtilities.h
|
||||||
|
|
||||||
|
$NoKeywords: $
|
||||||
|
=============================================================================*/
|
||||||
|
|
||||||
|
#ifndef __ALACBITUTILITIES_H
|
||||||
|
#define __ALACBITUTILITIES_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifndef MIN
|
||||||
|
#define MIN(x, y) ( (x)<(y) ?(x) :(y) )
|
||||||
|
#endif //MIN
|
||||||
|
#ifndef MAX
|
||||||
|
#define MAX(x, y) ( (x)>(y) ?(x): (y) )
|
||||||
|
#endif //MAX
|
||||||
|
|
||||||
|
#ifndef nil
|
||||||
|
#define nil NULL
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define RequireAction(condition, action) if (!(condition)) { action }
|
||||||
|
#define RequireActionSilent(condition, action) if (!(condition)) { action }
|
||||||
|
#define RequireNoErr(condition, action) if ((condition)) { action }
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
ALAC_noErr = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
|
||||||
|
ID_SCE = 0, /* Single Channel Element */
|
||||||
|
ID_CPE = 1, /* Channel Pair Element */
|
||||||
|
ID_CCE = 2, /* Coupling Channel Element */
|
||||||
|
ID_LFE = 3, /* LFE Channel Element */
|
||||||
|
ID_DSE = 4, /* not yet supported */
|
||||||
|
ID_PCE = 5,
|
||||||
|
ID_FIL = 6,
|
||||||
|
ID_END = 7
|
||||||
|
} ELEMENT_TYPE;
|
||||||
|
|
||||||
|
// types
|
||||||
|
typedef struct BitBuffer
|
||||||
|
{
|
||||||
|
uint8_t * cur;
|
||||||
|
uint8_t * end;
|
||||||
|
uint32_t bitIndex;
|
||||||
|
uint32_t byteSize;
|
||||||
|
|
||||||
|
} BitBuffer;
|
||||||
|
|
||||||
|
/*
|
||||||
|
BitBuffer routines
|
||||||
|
- these routines take a fixed size buffer and read/write to it
|
||||||
|
- bounds checking must be done by the client
|
||||||
|
*/
|
||||||
|
void BitBufferInit( BitBuffer * bits, uint8_t * buffer, uint32_t byteSize );
|
||||||
|
uint32_t BitBufferRead( BitBuffer * bits, uint8_t numBits ); // note: cannot read more than 16 bits at a time
|
||||||
|
uint8_t BitBufferReadSmall( BitBuffer * bits, uint8_t numBits );
|
||||||
|
uint8_t BitBufferReadOne( BitBuffer * bits );
|
||||||
|
uint32_t BitBufferPeek( BitBuffer * bits, uint8_t numBits ); // note: cannot read more than 16 bits at a time
|
||||||
|
uint32_t BitBufferPeekOne( BitBuffer * bits );
|
||||||
|
uint32_t BitBufferUnpackBERSize( BitBuffer * bits );
|
||||||
|
uint32_t BitBufferGetPosition( BitBuffer * bits );
|
||||||
|
void BitBufferByteAlign( BitBuffer * bits, int32_t addZeros );
|
||||||
|
void BitBufferAdvance( BitBuffer * bits, uint32_t numBits );
|
||||||
|
void BitBufferRewind( BitBuffer * bits, uint32_t numBits );
|
||||||
|
void BitBufferWrite( BitBuffer * bits, uint32_t value, uint32_t numBits );
|
||||||
|
void BitBufferReset( BitBuffer * bits);
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* __BITUTILITIES_H */
|
730
alac/codec/ALACDecoder.cpp
Normal file
730
alac/codec/ALACDecoder.cpp
Normal file
@ -0,0 +1,730 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
File: ALACDecoder.cpp
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "ALACDecoder.h"
|
||||||
|
|
||||||
|
#include "dplib.h"
|
||||||
|
#include "aglib.h"
|
||||||
|
#include "matrixlib.h"
|
||||||
|
|
||||||
|
#include "ALACBitUtilities.h"
|
||||||
|
#include "EndianPortable.h"
|
||||||
|
|
||||||
|
// constants/data
|
||||||
|
const uint32_t kMaxBitDepth = 32; // max allowed bit depth is 32
|
||||||
|
|
||||||
|
|
||||||
|
// prototypes
|
||||||
|
static void Zero16( int16_t * buffer, uint32_t numItems, uint32_t stride );
|
||||||
|
static void Zero24( uint8_t * buffer, uint32_t numItems, uint32_t stride );
|
||||||
|
static void Zero32( int32_t * buffer, uint32_t numItems, uint32_t stride );
|
||||||
|
|
||||||
|
/*
|
||||||
|
Constructor
|
||||||
|
*/
|
||||||
|
ALACDecoder::ALACDecoder() :
|
||||||
|
mMixBufferU( nil ),
|
||||||
|
mMixBufferV( nil ),
|
||||||
|
mPredictor( nil ),
|
||||||
|
mShiftBuffer( nil )
|
||||||
|
{
|
||||||
|
memset( &mConfig, 0, sizeof(mConfig) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Destructor
|
||||||
|
*/
|
||||||
|
ALACDecoder::~ALACDecoder()
|
||||||
|
{
|
||||||
|
// delete the matrix mixing buffers
|
||||||
|
if ( mMixBufferU )
|
||||||
|
{
|
||||||
|
free(mMixBufferU);
|
||||||
|
mMixBufferU = NULL;
|
||||||
|
}
|
||||||
|
if ( mMixBufferV )
|
||||||
|
{
|
||||||
|
free(mMixBufferV);
|
||||||
|
mMixBufferV = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the dynamic predictor's "corrector" buffer
|
||||||
|
// - note: mShiftBuffer shares memory with this buffer
|
||||||
|
if ( mPredictor )
|
||||||
|
{
|
||||||
|
free(mPredictor);
|
||||||
|
mPredictor = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Init()
|
||||||
|
- initialize the decoder with the given configuration
|
||||||
|
*/
|
||||||
|
int32_t ALACDecoder::Init( void * inMagicCookie, uint32_t inMagicCookieSize )
|
||||||
|
{
|
||||||
|
int32_t status = ALAC_noErr;
|
||||||
|
ALACSpecificConfig theConfig;
|
||||||
|
uint8_t * theActualCookie = (uint8_t *)inMagicCookie;
|
||||||
|
uint32_t theCookieBytesRemaining = inMagicCookieSize;
|
||||||
|
|
||||||
|
// For historical reasons the decoder needs to be resilient to magic cookies vended by older encoders.
|
||||||
|
// As specified in the ALACMagicCookieDescription.txt document, there may be additional data encapsulating
|
||||||
|
// the ALACSpecificConfig. This would consist of format ('frma') and 'alac' atoms which precede the
|
||||||
|
// ALACSpecificConfig.
|
||||||
|
// See ALACMagicCookieDescription.txt for additional documentation concerning the 'magic cookie'
|
||||||
|
|
||||||
|
// skip format ('frma') atom if present
|
||||||
|
if (theActualCookie[4] == 'f' && theActualCookie[5] == 'r' && theActualCookie[6] == 'm' && theActualCookie[7] == 'a')
|
||||||
|
{
|
||||||
|
theActualCookie += 12;
|
||||||
|
theCookieBytesRemaining -= 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip 'alac' atom header if present
|
||||||
|
if (theActualCookie[4] == 'a' && theActualCookie[5] == 'l' && theActualCookie[6] == 'a' && theActualCookie[7] == 'c')
|
||||||
|
{
|
||||||
|
theActualCookie += 12;
|
||||||
|
theCookieBytesRemaining -= 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the ALACSpecificConfig
|
||||||
|
if (theCookieBytesRemaining >= sizeof(ALACSpecificConfig))
|
||||||
|
{
|
||||||
|
theConfig.frameLength = Swap32BtoN(((ALACSpecificConfig *)theActualCookie)->frameLength);
|
||||||
|
theConfig.compatibleVersion = ((ALACSpecificConfig *)theActualCookie)->compatibleVersion;
|
||||||
|
theConfig.bitDepth = ((ALACSpecificConfig *)theActualCookie)->bitDepth;
|
||||||
|
theConfig.pb = ((ALACSpecificConfig *)theActualCookie)->pb;
|
||||||
|
theConfig.mb = ((ALACSpecificConfig *)theActualCookie)->mb;
|
||||||
|
theConfig.kb = ((ALACSpecificConfig *)theActualCookie)->kb;
|
||||||
|
theConfig.numChannels = ((ALACSpecificConfig *)theActualCookie)->numChannels;
|
||||||
|
theConfig.maxRun = Swap16BtoN(((ALACSpecificConfig *)theActualCookie)->maxRun);
|
||||||
|
theConfig.maxFrameBytes = Swap32BtoN(((ALACSpecificConfig *)theActualCookie)->maxFrameBytes);
|
||||||
|
theConfig.avgBitRate = Swap32BtoN(((ALACSpecificConfig *)theActualCookie)->avgBitRate);
|
||||||
|
theConfig.sampleRate = Swap32BtoN(((ALACSpecificConfig *)theActualCookie)->sampleRate);
|
||||||
|
|
||||||
|
mConfig = theConfig;
|
||||||
|
|
||||||
|
RequireAction( mConfig.compatibleVersion <= kALACVersion, return kALAC_ParamError; );
|
||||||
|
|
||||||
|
// allocate mix buffers
|
||||||
|
mMixBufferU = (int32_t *) calloc( mConfig.frameLength * sizeof(int32_t), 1 );
|
||||||
|
mMixBufferV = (int32_t *) calloc( mConfig.frameLength * sizeof(int32_t), 1 );
|
||||||
|
|
||||||
|
// allocate dynamic predictor buffer
|
||||||
|
mPredictor = (int32_t *) calloc( mConfig.frameLength * sizeof(int32_t), 1 );
|
||||||
|
|
||||||
|
// "shift off" buffer shares memory with predictor buffer
|
||||||
|
mShiftBuffer = (uint16_t *) mPredictor;
|
||||||
|
|
||||||
|
RequireAction( (mMixBufferU != nil) && (mMixBufferV != nil) && (mPredictor != nil),
|
||||||
|
status = kALAC_MemFullError; goto Exit; );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
status = kALAC_ParamError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip to Channel Layout Info
|
||||||
|
// theActualCookie += sizeof(ALACSpecificConfig);
|
||||||
|
|
||||||
|
// Currently, the Channel Layout Info portion of the magic cookie (as defined in the
|
||||||
|
// ALACMagicCookieDescription.txt document) is unused by the decoder.
|
||||||
|
|
||||||
|
Exit:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Decode()
|
||||||
|
- the decoded samples are interleaved into the output buffer in the order they arrive in
|
||||||
|
the bitstream
|
||||||
|
*/
|
||||||
|
int32_t ALACDecoder::Decode( BitBuffer * bits, uint8_t * sampleBuffer, uint32_t numSamples, uint32_t numChannels, uint32_t * outNumSamples )
|
||||||
|
{
|
||||||
|
BitBuffer shiftBits;
|
||||||
|
uint32_t bits1, bits2;
|
||||||
|
uint8_t tag;
|
||||||
|
uint8_t elementInstanceTag;
|
||||||
|
AGParamRec agParams;
|
||||||
|
uint32_t channelIndex;
|
||||||
|
int16_t coefsU[32]; // max possible size is 32 although NUMCOEPAIRS is the current limit
|
||||||
|
int16_t coefsV[32];
|
||||||
|
uint8_t numU, numV;
|
||||||
|
uint8_t mixBits;
|
||||||
|
int8_t mixRes;
|
||||||
|
uint16_t unusedHeader;
|
||||||
|
uint8_t escapeFlag;
|
||||||
|
uint32_t chanBits;
|
||||||
|
uint8_t bytesShifted;
|
||||||
|
uint32_t shift;
|
||||||
|
uint8_t modeU, modeV;
|
||||||
|
uint32_t denShiftU, denShiftV;
|
||||||
|
uint16_t pbFactorU, pbFactorV;
|
||||||
|
uint16_t pb;
|
||||||
|
int16_t * samples;
|
||||||
|
int16_t * out16;
|
||||||
|
uint8_t * out20;
|
||||||
|
uint8_t * out24;
|
||||||
|
int32_t * out32;
|
||||||
|
uint8_t headerByte;
|
||||||
|
uint8_t partialFrame;
|
||||||
|
uint32_t extraBits;
|
||||||
|
int32_t val;
|
||||||
|
uint32_t i, j;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
RequireAction( (bits != nil) && (sampleBuffer != nil) && (outNumSamples != nil), return kALAC_ParamError; );
|
||||||
|
RequireAction( numChannels > 0, return kALAC_ParamError; );
|
||||||
|
|
||||||
|
mActiveElements = 0;
|
||||||
|
channelIndex = 0;
|
||||||
|
|
||||||
|
samples = (int16_t *) sampleBuffer;
|
||||||
|
|
||||||
|
status = ALAC_noErr;
|
||||||
|
*outNumSamples = numSamples;
|
||||||
|
|
||||||
|
while ( status == ALAC_noErr )
|
||||||
|
{
|
||||||
|
// bail if we ran off the end of the buffer
|
||||||
|
RequireAction( bits->cur < bits->end, status = kALAC_ParamError; goto Exit; );
|
||||||
|
|
||||||
|
// copy global decode params for this element
|
||||||
|
pb = mConfig.pb;
|
||||||
|
|
||||||
|
// read element tag
|
||||||
|
tag = BitBufferReadSmall( bits, 3 );
|
||||||
|
switch ( tag )
|
||||||
|
{
|
||||||
|
case ID_SCE:
|
||||||
|
case ID_LFE:
|
||||||
|
{
|
||||||
|
// mono/LFE channel
|
||||||
|
elementInstanceTag = BitBufferReadSmall( bits, 4 );
|
||||||
|
mActiveElements |= (1u << elementInstanceTag);
|
||||||
|
|
||||||
|
// read the 12 unused header bits
|
||||||
|
unusedHeader = (uint16_t) BitBufferRead( bits, 12 );
|
||||||
|
RequireAction( unusedHeader == 0, status = kALAC_ParamError; goto Exit; );
|
||||||
|
|
||||||
|
// read the 1-bit "partial frame" flag, 2-bit "shift-off" flag & 1-bit "escape" flag
|
||||||
|
headerByte = (uint8_t) BitBufferRead( bits, 4 );
|
||||||
|
|
||||||
|
partialFrame = headerByte >> 3;
|
||||||
|
|
||||||
|
bytesShifted = (headerByte >> 1) & 0x3u;
|
||||||
|
RequireAction( bytesShifted != 3, status = kALAC_ParamError; goto Exit; );
|
||||||
|
|
||||||
|
shift = bytesShifted * 8;
|
||||||
|
|
||||||
|
escapeFlag = headerByte & 0x1;
|
||||||
|
|
||||||
|
chanBits = mConfig.bitDepth - (bytesShifted * 8);
|
||||||
|
|
||||||
|
// check for partial frame to override requested numSamples
|
||||||
|
if ( partialFrame != 0 )
|
||||||
|
{
|
||||||
|
numSamples = BitBufferRead( bits, 16 ) << 16;
|
||||||
|
numSamples |= BitBufferRead( bits, 16 );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( escapeFlag == 0 )
|
||||||
|
{
|
||||||
|
// compressed frame, read rest of parameters
|
||||||
|
mixBits = (uint8_t) BitBufferRead( bits, 8 );
|
||||||
|
mixRes = (int8_t) BitBufferRead( bits, 8 );
|
||||||
|
//Assert( (mixBits == 0) && (mixRes == 0) ); // no mixing for mono
|
||||||
|
|
||||||
|
headerByte = (uint8_t) BitBufferRead( bits, 8 );
|
||||||
|
modeU = headerByte >> 4;
|
||||||
|
denShiftU = headerByte & 0xfu;
|
||||||
|
|
||||||
|
headerByte = (uint8_t) BitBufferRead( bits, 8 );
|
||||||
|
pbFactorU = headerByte >> 5;
|
||||||
|
numU = headerByte & 0x1fu;
|
||||||
|
|
||||||
|
for ( i = 0; i < numU; i++ )
|
||||||
|
coefsU[i] = (int16_t) BitBufferRead( bits, 16 );
|
||||||
|
|
||||||
|
// if shift active, skip the the shift buffer but remember where it starts
|
||||||
|
if ( bytesShifted != 0 )
|
||||||
|
{
|
||||||
|
shiftBits = *bits;
|
||||||
|
BitBufferAdvance( bits, (bytesShifted * 8) * numSamples );
|
||||||
|
}
|
||||||
|
|
||||||
|
// decompress
|
||||||
|
set_ag_params( &agParams, mConfig.mb, (pb * pbFactorU) / 4, mConfig.kb, numSamples, numSamples, mConfig.maxRun );
|
||||||
|
status = dyn_decomp( &agParams, bits, mPredictor, numSamples, chanBits, &bits1 );
|
||||||
|
RequireNoErr( status, goto Exit; );
|
||||||
|
|
||||||
|
if ( modeU == 0 )
|
||||||
|
{
|
||||||
|
unpc_block( mPredictor, mMixBufferU, numSamples, &coefsU[0], numU, chanBits, denShiftU );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// the special "numActive == 31" mode can be done in-place
|
||||||
|
unpc_block( mPredictor, mPredictor, numSamples, nil, 31, chanBits, 0 );
|
||||||
|
unpc_block( mPredictor, mMixBufferU, numSamples, &coefsU[0], numU, chanBits, denShiftU );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//Assert( bytesShifted == 0 );
|
||||||
|
|
||||||
|
// uncompressed frame, copy data into the mix buffer to use common output code
|
||||||
|
shift = 32 - chanBits;
|
||||||
|
if ( chanBits <= 16 )
|
||||||
|
{
|
||||||
|
for ( i = 0; i < numSamples; i++ )
|
||||||
|
{
|
||||||
|
val = (int32_t) BitBufferRead( bits, (uint8_t) chanBits );
|
||||||
|
val = (val << shift) >> shift;
|
||||||
|
mMixBufferU[i] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// BitBufferRead() can't read more than 16 bits at a time so break up the reads
|
||||||
|
extraBits = chanBits - 16;
|
||||||
|
for ( i = 0; i < numSamples; i++ )
|
||||||
|
{
|
||||||
|
val = (int32_t) BitBufferRead( bits, 16 );
|
||||||
|
val = (val << 16) >> shift;
|
||||||
|
mMixBufferU[i] = val | BitBufferRead( bits, (uint8_t) extraBits );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mixBits = mixRes = 0;
|
||||||
|
bits1 = chanBits * numSamples;
|
||||||
|
bytesShifted = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// now read the shifted values into the shift buffer
|
||||||
|
if ( bytesShifted != 0 )
|
||||||
|
{
|
||||||
|
shift = bytesShifted * 8;
|
||||||
|
//Assert( shift <= 16 );
|
||||||
|
|
||||||
|
for ( i = 0; i < numSamples; i++ )
|
||||||
|
mShiftBuffer[i] = (uint16_t) BitBufferRead( &shiftBits, (uint8_t) shift );
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert 32-bit integers into output buffer
|
||||||
|
switch ( mConfig.bitDepth )
|
||||||
|
{
|
||||||
|
case 16:
|
||||||
|
out16 = &((int16_t *)sampleBuffer)[channelIndex];
|
||||||
|
for ( i = 0, j = 0; i < numSamples; i++, j += numChannels )
|
||||||
|
out16[j] = (int16_t) mMixBufferU[i];
|
||||||
|
break;
|
||||||
|
case 20:
|
||||||
|
out20 = (uint8_t *)sampleBuffer + (channelIndex * 3);
|
||||||
|
copyPredictorTo20( mMixBufferU, out20, numChannels, numSamples );
|
||||||
|
break;
|
||||||
|
case 24:
|
||||||
|
out24 = (uint8_t *)sampleBuffer + (channelIndex * 3);
|
||||||
|
if ( bytesShifted != 0 )
|
||||||
|
copyPredictorTo24Shift( mMixBufferU, mShiftBuffer, out24, numChannels, numSamples, bytesShifted );
|
||||||
|
else
|
||||||
|
copyPredictorTo24( mMixBufferU, out24, numChannels, numSamples );
|
||||||
|
break;
|
||||||
|
case 32:
|
||||||
|
out32 = &((int32_t *)sampleBuffer)[channelIndex];
|
||||||
|
if ( bytesShifted != 0 )
|
||||||
|
copyPredictorTo32Shift( mMixBufferU, mShiftBuffer, out32, numChannels, numSamples, bytesShifted );
|
||||||
|
else
|
||||||
|
copyPredictorTo32( mMixBufferU, out32, numChannels, numSamples);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
channelIndex += 1;
|
||||||
|
*outNumSamples = numSamples;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ID_CPE:
|
||||||
|
{
|
||||||
|
// if decoding this pair would take us over the max channels limit, bail
|
||||||
|
if ( (channelIndex + 2) > numChannels )
|
||||||
|
goto NoMoreChannels;
|
||||||
|
|
||||||
|
// stereo channel pair
|
||||||
|
elementInstanceTag = BitBufferReadSmall( bits, 4 );
|
||||||
|
mActiveElements |= (1u << elementInstanceTag);
|
||||||
|
|
||||||
|
// read the 12 unused header bits
|
||||||
|
unusedHeader = (uint16_t) BitBufferRead( bits, 12 );
|
||||||
|
RequireAction( unusedHeader == 0, status = kALAC_ParamError; goto Exit; );
|
||||||
|
|
||||||
|
// read the 1-bit "partial frame" flag, 2-bit "shift-off" flag & 1-bit "escape" flag
|
||||||
|
headerByte = (uint8_t) BitBufferRead( bits, 4 );
|
||||||
|
|
||||||
|
partialFrame = headerByte >> 3;
|
||||||
|
|
||||||
|
bytesShifted = (headerByte >> 1) & 0x3u;
|
||||||
|
RequireAction( bytesShifted != 3, status = kALAC_ParamError; goto Exit; );
|
||||||
|
|
||||||
|
shift = bytesShifted * 8;
|
||||||
|
|
||||||
|
escapeFlag = headerByte & 0x1;
|
||||||
|
|
||||||
|
chanBits = mConfig.bitDepth - (bytesShifted * 8) + 1;
|
||||||
|
|
||||||
|
// check for partial frame length to override requested numSamples
|
||||||
|
if ( partialFrame != 0 )
|
||||||
|
{
|
||||||
|
numSamples = BitBufferRead( bits, 16 ) << 16;
|
||||||
|
numSamples |= BitBufferRead( bits, 16 );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( escapeFlag == 0 )
|
||||||
|
{
|
||||||
|
// compressed frame, read rest of parameters
|
||||||
|
mixBits = (uint8_t) BitBufferRead( bits, 8 );
|
||||||
|
mixRes = (int8_t) BitBufferRead( bits, 8 );
|
||||||
|
|
||||||
|
headerByte = (uint8_t) BitBufferRead( bits, 8 );
|
||||||
|
modeU = headerByte >> 4;
|
||||||
|
denShiftU = headerByte & 0xfu;
|
||||||
|
|
||||||
|
headerByte = (uint8_t) BitBufferRead( bits, 8 );
|
||||||
|
pbFactorU = headerByte >> 5;
|
||||||
|
numU = headerByte & 0x1fu;
|
||||||
|
for ( i = 0; i < numU; i++ )
|
||||||
|
coefsU[i] = (int16_t) BitBufferRead( bits, 16 );
|
||||||
|
|
||||||
|
headerByte = (uint8_t) BitBufferRead( bits, 8 );
|
||||||
|
modeV = headerByte >> 4;
|
||||||
|
denShiftV = headerByte & 0xfu;
|
||||||
|
|
||||||
|
headerByte = (uint8_t) BitBufferRead( bits, 8 );
|
||||||
|
pbFactorV = headerByte >> 5;
|
||||||
|
numV = headerByte & 0x1fu;
|
||||||
|
for ( i = 0; i < numV; i++ )
|
||||||
|
coefsV[i] = (int16_t) BitBufferRead( bits, 16 );
|
||||||
|
|
||||||
|
// if shift active, skip the interleaved shifted values but remember where they start
|
||||||
|
if ( bytesShifted != 0 )
|
||||||
|
{
|
||||||
|
shiftBits = *bits;
|
||||||
|
BitBufferAdvance( bits, (bytesShifted * 8) * 2 * numSamples );
|
||||||
|
}
|
||||||
|
|
||||||
|
// decompress and run predictor for "left" channel
|
||||||
|
set_ag_params( &agParams, mConfig.mb, (pb * pbFactorU) / 4, mConfig.kb, numSamples, numSamples, mConfig.maxRun );
|
||||||
|
status = dyn_decomp( &agParams, bits, mPredictor, numSamples, chanBits, &bits1 );
|
||||||
|
RequireNoErr( status, goto Exit; );
|
||||||
|
|
||||||
|
if ( modeU == 0 )
|
||||||
|
{
|
||||||
|
unpc_block( mPredictor, mMixBufferU, numSamples, &coefsU[0], numU, chanBits, denShiftU );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// the special "numActive == 31" mode can be done in-place
|
||||||
|
unpc_block( mPredictor, mPredictor, numSamples, nil, 31, chanBits, 0 );
|
||||||
|
unpc_block( mPredictor, mMixBufferU, numSamples, &coefsU[0], numU, chanBits, denShiftU );
|
||||||
|
}
|
||||||
|
|
||||||
|
// decompress and run predictor for "right" channel
|
||||||
|
set_ag_params( &agParams, mConfig.mb, (pb * pbFactorV) / 4, mConfig.kb, numSamples, numSamples, mConfig.maxRun );
|
||||||
|
status = dyn_decomp( &agParams, bits, mPredictor, numSamples, chanBits, &bits2 );
|
||||||
|
RequireNoErr( status, goto Exit; );
|
||||||
|
|
||||||
|
if ( modeV == 0 )
|
||||||
|
{
|
||||||
|
unpc_block( mPredictor, mMixBufferV, numSamples, &coefsV[0], numV, chanBits, denShiftV );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// the special "numActive == 31" mode can be done in-place
|
||||||
|
unpc_block( mPredictor, mPredictor, numSamples, nil, 31, chanBits, 0 );
|
||||||
|
unpc_block( mPredictor, mMixBufferV, numSamples, &coefsV[0], numV, chanBits, denShiftV );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//Assert( bytesShifted == 0 );
|
||||||
|
|
||||||
|
// uncompressed frame, copy data into the mix buffers to use common output code
|
||||||
|
chanBits = mConfig.bitDepth;
|
||||||
|
shift = 32 - chanBits;
|
||||||
|
if ( chanBits <= 16 )
|
||||||
|
{
|
||||||
|
for ( i = 0; i < numSamples; i++ )
|
||||||
|
{
|
||||||
|
val = (int32_t) BitBufferRead( bits, (uint8_t) chanBits );
|
||||||
|
val = (val << shift) >> shift;
|
||||||
|
mMixBufferU[i] = val;
|
||||||
|
|
||||||
|
val = (int32_t) BitBufferRead( bits, (uint8_t) chanBits );
|
||||||
|
val = (val << shift) >> shift;
|
||||||
|
mMixBufferV[i] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// BitBufferRead() can't read more than 16 bits at a time so break up the reads
|
||||||
|
extraBits = chanBits - 16;
|
||||||
|
for ( i = 0; i < numSamples; i++ )
|
||||||
|
{
|
||||||
|
val = (int32_t) BitBufferRead( bits, 16 );
|
||||||
|
val = (val << 16) >> shift;
|
||||||
|
mMixBufferU[i] = val | BitBufferRead( bits, (uint8_t)extraBits );
|
||||||
|
|
||||||
|
val = (int32_t) BitBufferRead( bits, 16 );
|
||||||
|
val = (val << 16) >> shift;
|
||||||
|
mMixBufferV[i] = val | BitBufferRead( bits, (uint8_t)extraBits );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bits1 = chanBits * numSamples;
|
||||||
|
bits2 = chanBits * numSamples;
|
||||||
|
mixBits = mixRes = 0;
|
||||||
|
bytesShifted = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// now read the shifted values into the shift buffer
|
||||||
|
if ( bytesShifted != 0 )
|
||||||
|
{
|
||||||
|
shift = bytesShifted * 8;
|
||||||
|
//Assert( shift <= 16 );
|
||||||
|
|
||||||
|
for ( i = 0; i < (numSamples * 2); i += 2 )
|
||||||
|
{
|
||||||
|
mShiftBuffer[i + 0] = (uint16_t) BitBufferRead( &shiftBits, (uint8_t) shift );
|
||||||
|
mShiftBuffer[i + 1] = (uint16_t) BitBufferRead( &shiftBits, (uint8_t) shift );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// un-mix the data and convert to output format
|
||||||
|
// - note that mixRes = 0 means just interleave so we use that path for uncompressed frames
|
||||||
|
switch ( mConfig.bitDepth )
|
||||||
|
{
|
||||||
|
case 16:
|
||||||
|
out16 = &((int16_t *)sampleBuffer)[channelIndex];
|
||||||
|
unmix16( mMixBufferU, mMixBufferV, out16, numChannels, numSamples, mixBits, mixRes );
|
||||||
|
break;
|
||||||
|
case 20:
|
||||||
|
out20 = (uint8_t *)sampleBuffer + (channelIndex * 3);
|
||||||
|
unmix20( mMixBufferU, mMixBufferV, out20, numChannels, numSamples, mixBits, mixRes );
|
||||||
|
break;
|
||||||
|
case 24:
|
||||||
|
out24 = (uint8_t *)sampleBuffer + (channelIndex * 3);
|
||||||
|
unmix24( mMixBufferU, mMixBufferV, out24, numChannels, numSamples,
|
||||||
|
mixBits, mixRes, mShiftBuffer, bytesShifted );
|
||||||
|
break;
|
||||||
|
case 32:
|
||||||
|
out32 = &((int32_t *)sampleBuffer)[channelIndex];
|
||||||
|
unmix32( mMixBufferU, mMixBufferV, out32, numChannels, numSamples,
|
||||||
|
mixBits, mixRes, mShiftBuffer, bytesShifted );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
channelIndex += 2;
|
||||||
|
*outNumSamples = numSamples;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ID_CCE:
|
||||||
|
case ID_PCE:
|
||||||
|
{
|
||||||
|
// unsupported element, bail
|
||||||
|
//AssertNoErr( tag );
|
||||||
|
status = kALAC_ParamError;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ID_DSE:
|
||||||
|
{
|
||||||
|
// data stream element -- parse but ignore
|
||||||
|
status = this->DataStreamElement( bits );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ID_FIL:
|
||||||
|
{
|
||||||
|
// fill element -- parse but ignore
|
||||||
|
status = this->FillElement( bits );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ID_END:
|
||||||
|
{
|
||||||
|
// frame end, all done so byte align the frame and check for overruns
|
||||||
|
BitBufferByteAlign( bits, false );
|
||||||
|
//Assert( bits->cur == bits->end );
|
||||||
|
goto Exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if ! DEBUG
|
||||||
|
// if we've decoded all of our channels, bail (but not in debug b/c we want to know if we're seeing bad bits)
|
||||||
|
// - this also protects us if the config does not match the bitstream or crap data bits follow the audio bits
|
||||||
|
if ( channelIndex >= numChannels )
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
NoMoreChannels:
|
||||||
|
|
||||||
|
// if we get here and haven't decoded all of the requested channels, fill the remaining channels with zeros
|
||||||
|
for ( ; channelIndex < numChannels; channelIndex++ )
|
||||||
|
{
|
||||||
|
switch ( mConfig.bitDepth )
|
||||||
|
{
|
||||||
|
case 16:
|
||||||
|
{
|
||||||
|
int16_t * fill16 = &((int16_t *)sampleBuffer)[channelIndex];
|
||||||
|
Zero16( fill16, numSamples, numChannels );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 24:
|
||||||
|
{
|
||||||
|
uint8_t * fill24 = (uint8_t *)sampleBuffer + (channelIndex * 3);
|
||||||
|
Zero24( fill24, numSamples, numChannels );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 32:
|
||||||
|
{
|
||||||
|
int32_t * fill32 = &((int32_t *)sampleBuffer)[channelIndex];
|
||||||
|
Zero32( fill32, numSamples, numChannels );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Exit:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if PRAGMA_MARK
|
||||||
|
#pragma mark -
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
FillElement()
|
||||||
|
- they're just filler so we don't need 'em
|
||||||
|
*/
|
||||||
|
int32_t ALACDecoder::FillElement( BitBuffer * bits )
|
||||||
|
{
|
||||||
|
int16_t count;
|
||||||
|
|
||||||
|
// 4-bit count or (4-bit + 8-bit count) if 4-bit count == 15
|
||||||
|
// - plus this weird -1 thing I still don't fully understand
|
||||||
|
count = BitBufferReadSmall( bits, 4 );
|
||||||
|
if ( count == 15 )
|
||||||
|
count += (int16_t) BitBufferReadSmall( bits, 8 ) - 1;
|
||||||
|
|
||||||
|
BitBufferAdvance( bits, count * 8 );
|
||||||
|
|
||||||
|
RequireAction( bits->cur <= bits->end, return kALAC_ParamError; );
|
||||||
|
|
||||||
|
return ALAC_noErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
DataStreamElement()
|
||||||
|
- we don't care about data stream elements so just skip them
|
||||||
|
*/
|
||||||
|
int32_t ALACDecoder::DataStreamElement( BitBuffer * bits )
|
||||||
|
{
|
||||||
|
uint8_t element_instance_tag;
|
||||||
|
int32_t data_byte_align_flag;
|
||||||
|
uint16_t count;
|
||||||
|
|
||||||
|
// the tag associates this data stream element with a given audio element
|
||||||
|
element_instance_tag = BitBufferReadSmall( bits, 4 );
|
||||||
|
|
||||||
|
data_byte_align_flag = BitBufferReadOne( bits );
|
||||||
|
|
||||||
|
// 8-bit count or (8-bit + 8-bit count) if 8-bit count == 255
|
||||||
|
count = BitBufferReadSmall( bits, 8 );
|
||||||
|
if ( count == 255 )
|
||||||
|
count += BitBufferReadSmall( bits, 8 );
|
||||||
|
|
||||||
|
// the align flag means the bitstream should be byte-aligned before reading the following data bytes
|
||||||
|
if ( data_byte_align_flag )
|
||||||
|
BitBufferByteAlign( bits, false );
|
||||||
|
|
||||||
|
// skip the data bytes
|
||||||
|
BitBufferAdvance( bits, count * 8 );
|
||||||
|
|
||||||
|
RequireAction( bits->cur <= bits->end, return kALAC_ParamError; );
|
||||||
|
|
||||||
|
return ALAC_noErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ZeroN()
|
||||||
|
- helper routines to clear out output channel buffers when decoding fewer channels than requested
|
||||||
|
*/
|
||||||
|
static void Zero16( int16_t * buffer, uint32_t numItems, uint32_t stride )
|
||||||
|
{
|
||||||
|
if ( stride == 1 )
|
||||||
|
{
|
||||||
|
memset( buffer, 0, numItems * sizeof(int16_t) );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for ( uint32_t index = 0; index < (numItems * stride); index += stride )
|
||||||
|
buffer[index] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Zero24( uint8_t * buffer, uint32_t numItems, uint32_t stride )
|
||||||
|
{
|
||||||
|
if ( stride == 1 )
|
||||||
|
{
|
||||||
|
memset( buffer, 0, numItems * 3 );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for ( uint32_t index = 0; index < (numItems * stride * 3); index += (stride * 3) )
|
||||||
|
{
|
||||||
|
buffer[index + 0] = 0;
|
||||||
|
buffer[index + 1] = 0;
|
||||||
|
buffer[index + 2] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Zero32( int32_t * buffer, uint32_t numItems, uint32_t stride )
|
||||||
|
{
|
||||||
|
if ( stride == 1 )
|
||||||
|
{
|
||||||
|
memset( buffer, 0, numItems * sizeof(int32_t) );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for ( uint32_t index = 0; index < (numItems * stride); index += stride )
|
||||||
|
buffer[index] = 0;
|
||||||
|
}
|
||||||
|
}
|
1425
alac/codec/ALACEncoder.cpp
Normal file
1425
alac/codec/ALACEncoder.cpp
Normal file
File diff suppressed because it is too large
Load Diff
92
alac/codec/ALACEncoder.h
Normal file
92
alac/codec/ALACEncoder.h
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
File: ALACEncoder.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "ALACAudioTypes.h"
|
||||||
|
|
||||||
|
|
||||||
|
struct BitBuffer;
|
||||||
|
|
||||||
|
class ALACEncoder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ALACEncoder();
|
||||||
|
virtual ~ALACEncoder();
|
||||||
|
|
||||||
|
virtual int32_t Encode(AudioFormatDescription theInputFormat, AudioFormatDescription theOutputFormat,
|
||||||
|
unsigned char * theReadBuffer, unsigned char * theWriteBuffer, int32_t * ioNumBytes);
|
||||||
|
virtual int32_t Finish( );
|
||||||
|
|
||||||
|
void SetFastMode( bool fast ) { mFastMode = fast; };
|
||||||
|
|
||||||
|
// this must be called *before* InitializeEncoder()
|
||||||
|
void SetFrameSize( uint32_t frameSize ) { mFrameSize = frameSize; };
|
||||||
|
|
||||||
|
void GetConfig( ALACSpecificConfig & config );
|
||||||
|
uint32_t GetMagicCookieSize(uint32_t inNumChannels);
|
||||||
|
void GetMagicCookie( void * config, uint32_t * ioSize );
|
||||||
|
|
||||||
|
virtual int32_t InitializeEncoder(AudioFormatDescription theOutputFormat);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void GetSourceFormat( const AudioFormatDescription * source, AudioFormatDescription * output );
|
||||||
|
|
||||||
|
int32_t EncodeStereo( struct BitBuffer * bitstream, void * input, uint32_t stride, uint32_t channelIndex, uint32_t numSamples );
|
||||||
|
int32_t EncodeStereoFast( struct BitBuffer * bitstream, void * input, uint32_t stride, uint32_t channelIndex, uint32_t numSamples );
|
||||||
|
int32_t EncodeStereoEscape( struct BitBuffer * bitstream, void * input, uint32_t stride, uint32_t numSamples );
|
||||||
|
int32_t EncodeMono( struct BitBuffer * bitstream, void * input, uint32_t stride, uint32_t channelIndex, uint32_t numSamples );
|
||||||
|
|
||||||
|
|
||||||
|
// ALAC encoder parameters
|
||||||
|
int16_t mBitDepth;
|
||||||
|
bool mFastMode;
|
||||||
|
|
||||||
|
// encoding state
|
||||||
|
int16_t mLastMixRes[kALACMaxChannels];
|
||||||
|
|
||||||
|
// encoding buffers
|
||||||
|
int32_t * mMixBufferU;
|
||||||
|
int32_t * mMixBufferV;
|
||||||
|
int32_t * mPredictorU;
|
||||||
|
int32_t * mPredictorV;
|
||||||
|
uint16_t * mShiftBufferUV;
|
||||||
|
|
||||||
|
uint8_t * mWorkBuffer;
|
||||||
|
|
||||||
|
// per-channel coefficients buffers
|
||||||
|
int16_t mCoefsU[kALACMaxChannels][kALACMaxSearches][kALACMaxCoefs];
|
||||||
|
int16_t mCoefsV[kALACMaxChannels][kALACMaxSearches][kALACMaxCoefs];
|
||||||
|
|
||||||
|
// encoding statistics
|
||||||
|
uint32_t mTotalBytesGenerated;
|
||||||
|
uint32_t mAvgBitRate;
|
||||||
|
uint32_t mMaxFrameBytes;
|
||||||
|
uint32_t mFrameSize;
|
||||||
|
uint32_t mMaxOutputBytes;
|
||||||
|
uint32_t mNumChannels;
|
||||||
|
uint32_t mOutputSampleRate;
|
||||||
|
};
|
335
alac/codec/APPLE_LICENSE.txt
Normal file
335
alac/codec/APPLE_LICENSE.txt
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
APPLE PUBLIC SOURCE LICENSE
|
||||||
|
Version 2.0 - August 6, 2003
|
||||||
|
|
||||||
|
Please read this License carefully before downloading this software. By
|
||||||
|
downloading or using this software, you are agreeing to be bound by the terms
|
||||||
|
of this License. If you do not or cannot agree to the terms of this License,
|
||||||
|
please do not download or use the software.
|
||||||
|
|
||||||
|
Apple Note: In January 2007, Apple changed its corporate name from "Apple
|
||||||
|
Computer, Inc." to "Apple Inc." This change has been reflected below and
|
||||||
|
copyright years updated, but no other changes have been made to the APSL 2.0.
|
||||||
|
|
||||||
|
1. General; Definitions. This License applies to any program or other
|
||||||
|
work which Apple Inc. ("Apple") makes publicly available and which contains a
|
||||||
|
notice placed by Apple identifying such program or work as "Original Code" and
|
||||||
|
stating that it is subject to the terms of this Apple Public Source License
|
||||||
|
version 2.0 ("License"). As used in this License:
|
||||||
|
|
||||||
|
1.1 "Applicable Patent Rights" mean: (a) in the case where Apple is the
|
||||||
|
grantor of rights, (i) claims of patents that are now or hereafter acquired,
|
||||||
|
owned by or assigned to Apple and (ii) that cover subject matter contained in
|
||||||
|
the Original Code, but only to the extent necessary to use, reproduce and/or
|
||||||
|
distribute the Original Code without infringement; and (b) in the case where
|
||||||
|
You are the grantor of rights, (i) claims of patents that are now or hereafter
|
||||||
|
acquired, owned by or assigned to You and (ii) that cover subject matter in
|
||||||
|
Your Modifications, taken alone or in combination with Original Code.
|
||||||
|
|
||||||
|
1.2 "Contributor" means any person or entity that creates or contributes to
|
||||||
|
the creation of Modifications.
|
||||||
|
|
||||||
|
1.3 "Covered Code" means the Original Code, Modifications, the combination
|
||||||
|
of Original Code and any Modifications, and/or any respective portions thereof.
|
||||||
|
|
||||||
|
1.4 "Externally Deploy" means: (a) to sublicense, distribute or otherwise
|
||||||
|
make Covered Code available, directly or indirectly, to anyone other than You;
|
||||||
|
and/or (b) to use Covered Code, alone or as part of a Larger Work, in any way
|
||||||
|
to provide a service, including but not limited to delivery of content, through
|
||||||
|
electronic communication with a client other than You.
|
||||||
|
|
||||||
|
1.5 "Larger Work" means a work which combines Covered Code or portions
|
||||||
|
thereof with code not governed by the terms of this License.
|
||||||
|
|
||||||
|
1.6 "Modifications" mean any addition to, deletion from, and/or change to,
|
||||||
|
the substance and/or structure of the Original Code, any previous
|
||||||
|
Modifications, the combination of Original Code and any previous Modifications,
|
||||||
|
and/or any respective portions thereof. When code is released as a series of
|
||||||
|
files, a Modification is: (a) any addition to or deletion from the contents of
|
||||||
|
a file containing Covered Code; and/or (b) any new file or other representation
|
||||||
|
of computer program statements that contains any part of Covered Code.
|
||||||
|
|
||||||
|
1.7 "Original Code" means (a) the Source Code of a program or other work as
|
||||||
|
originally made available by Apple under this License, including the Source
|
||||||
|
Code of any updates or upgrades to such programs or works made available by
|
||||||
|
Apple under this License, and that has been expressly identified by Apple as
|
||||||
|
such in the header file(s) of such work; and (b) the object code compiled from
|
||||||
|
such Source Code and originally made available by Apple under this License
|
||||||
|
|
||||||
|
1.8 "Source Code" means the human readable form of a program or other work
|
||||||
|
that is suitable for making modifications to it, including all modules it
|
||||||
|
contains, plus any associated interface definition files, scripts used to
|
||||||
|
control compilation and installation of an executable (object code).
|
||||||
|
|
||||||
|
1.9 "You" or "Your" means an individual or a legal entity exercising rights
|
||||||
|
under this License. For legal entities, "You" or "Your" includes any entity
|
||||||
|
which controls, is controlled by, or is under common control with, You, where
|
||||||
|
"control" means (a) the power, direct or indirect, to cause the direction or
|
||||||
|
management of such entity, whether by contract or otherwise, or (b) ownership
|
||||||
|
of fifty percent (50%) or more of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. Permitted Uses; Conditions & Restrictions. Subject to the terms and
|
||||||
|
conditions of this License, Apple hereby grants You, effective on the date You
|
||||||
|
accept this License and download the Original Code, a world-wide, royalty-free,
|
||||||
|
non-exclusive license, to the extent of Apple's Applicable Patent Rights and
|
||||||
|
copyrights covering the Original Code, to do the following:
|
||||||
|
|
||||||
|
2.1 Unmodified Code. You may use, reproduce, display, perform, internally
|
||||||
|
distribute within Your organization, and Externally Deploy verbatim, unmodified
|
||||||
|
copies of the Original Code, for commercial or non-commercial purposes,
|
||||||
|
provided that in each instance:
|
||||||
|
|
||||||
|
(a) You must retain and reproduce in all copies of Original Code the
|
||||||
|
copyright and other proprietary notices and disclaimers of Apple as they appear
|
||||||
|
in the Original Code, and keep intact all notices in the Original Code that
|
||||||
|
refer to this License; and
|
||||||
|
|
||||||
|
(b) You must include a copy of this License with every copy of Source Code
|
||||||
|
of Covered Code and documentation You distribute or Externally Deploy, and You
|
||||||
|
may not offer or impose any terms on such Source Code that alter or restrict
|
||||||
|
this License or the recipients' rights hereunder, except as permitted under
|
||||||
|
Section 6.
|
||||||
|
|
||||||
|
2.2 Modified Code. You may modify Covered Code and use, reproduce,
|
||||||
|
display, perform, internally distribute within Your organization, and
|
||||||
|
Externally Deploy Your Modifications and Covered Code, for commercial or
|
||||||
|
non-commercial purposes, provided that in each instance You also meet all of
|
||||||
|
these conditions:
|
||||||
|
|
||||||
|
(a) You must satisfy all the conditions of Section 2.1 with respect to the
|
||||||
|
Source Code of the Covered Code;
|
||||||
|
|
||||||
|
(b) You must duplicate, to the extent it does not already exist, the notice
|
||||||
|
in Exhibit A in each file of the Source Code of all Your Modifications, and
|
||||||
|
cause the modified files to carry prominent notices stating that You changed
|
||||||
|
the files and the date of any change; and
|
||||||
|
|
||||||
|
(c) If You Externally Deploy Your Modifications, You must make Source Code
|
||||||
|
of all Your Externally Deployed Modifications either available to those to whom
|
||||||
|
You have Externally Deployed Your Modifications, or publicly available. Source
|
||||||
|
Code of Your Externally Deployed Modifications must be released under the terms
|
||||||
|
set forth in this License, including the license grants set forth in Section 3
|
||||||
|
below, for as long as you Externally Deploy the Covered Code or twelve (12)
|
||||||
|
months from the date of initial External Deployment, whichever is longer. You
|
||||||
|
should preferably distribute the Source Code of Your Externally Deployed
|
||||||
|
Modifications electronically (e.g. download from a web site).
|
||||||
|
|
||||||
|
2.3 Distribution of Executable Versions. In addition, if You Externally
|
||||||
|
Deploy Covered Code (Original Code and/or Modifications) in object code,
|
||||||
|
executable form only, You must include a prominent notice, in the code itself
|
||||||
|
as well as in related documentation, stating that Source Code of the Covered
|
||||||
|
Code is available under the terms of this License with information on how and
|
||||||
|
where to obtain such Source Code.
|
||||||
|
|
||||||
|
2.4 Third Party Rights. You expressly acknowledge and agree that although
|
||||||
|
Apple and each Contributor grants the licenses to their respective portions of
|
||||||
|
the Covered Code set forth herein, no assurances are provided by Apple or any
|
||||||
|
Contributor that the Covered Code does not infringe the patent or other
|
||||||
|
intellectual property rights of any other entity. Apple and each Contributor
|
||||||
|
disclaim any liability to You for claims brought by any other entity based on
|
||||||
|
infringement of intellectual property rights or otherwise. As a condition to
|
||||||
|
exercising the rights and licenses granted hereunder, You hereby assume sole
|
||||||
|
responsibility to secure any other intellectual property rights needed, if any.
|
||||||
|
For example, if a third party patent license is required to allow You to
|
||||||
|
distribute the Covered Code, it is Your responsibility to acquire that license
|
||||||
|
before distributing the Covered Code.
|
||||||
|
|
||||||
|
3. Your Grants. In consideration of, and as a condition to, the licenses
|
||||||
|
granted to You under this License, You hereby grant to any person or entity
|
||||||
|
receiving or distributing Covered Code under this License a non-exclusive,
|
||||||
|
royalty-free, perpetual, irrevocable license, under Your Applicable Patent
|
||||||
|
Rights and other intellectual property rights (other than patent) owned or
|
||||||
|
controlled by You, to use, reproduce, display, perform, modify, sublicense,
|
||||||
|
distribute and Externally Deploy Your Modifications of the same scope and
|
||||||
|
extent as Apple's licenses under Sections 2.1 and 2.2 above.
|
||||||
|
|
||||||
|
4. Larger Works. You may create a Larger Work by combining Covered Code
|
||||||
|
with other code not governed by the terms of this License and distribute the
|
||||||
|
Larger Work as a single product. In each such instance, You must make sure the
|
||||||
|
requirements of this License are fulfilled for the Covered Code or any portion
|
||||||
|
thereof.
|
||||||
|
|
||||||
|
5. Limitations on Patent License. Except as expressly stated in Section
|
||||||
|
2, no other patent rights, express or implied, are granted by Apple herein.
|
||||||
|
Modifications and/or Larger Works may require additional patent licenses from
|
||||||
|
Apple which Apple may grant in its sole discretion.
|
||||||
|
|
||||||
|
6. Additional Terms. You may choose to offer, and to charge a fee for,
|
||||||
|
warranty, support, indemnity or liability obligations and/or other rights
|
||||||
|
consistent with the scope of the license granted herein ("Additional Terms") to
|
||||||
|
one or more recipients of Covered Code. However, You may do so only on Your own
|
||||||
|
behalf and as Your sole responsibility, and not on behalf of Apple or any
|
||||||
|
Contributor. You must obtain the recipient's agreement that any such Additional
|
||||||
|
Terms are offered by You alone, and You hereby agree to indemnify, defend and
|
||||||
|
hold Apple and every Contributor harmless for any liability incurred by or
|
||||||
|
claims asserted against Apple or such Contributor by reason of any such
|
||||||
|
Additional Terms.
|
||||||
|
|
||||||
|
7. Versions of the License. Apple may publish revised and/or new versions
|
||||||
|
of this License from time to time. Each version will be given a distinguishing
|
||||||
|
version number. Once Original Code has been published under a particular
|
||||||
|
version of this License, You may continue to use it under the terms of that
|
||||||
|
version. You may also choose to use such Original Code under the terms of any
|
||||||
|
subsequent version of this License published by Apple. No one other than Apple
|
||||||
|
has the right to modify the terms applicable to Covered Code created under this
|
||||||
|
License.
|
||||||
|
|
||||||
|
8. NO WARRANTY OR SUPPORT. The Covered Code may contain in whole or in
|
||||||
|
part pre-release, untested, or not fully tested works. The Covered Code may
|
||||||
|
contain errors that could cause failures or loss of data, and may be incomplete
|
||||||
|
or contain inaccuracies. You expressly acknowledge and agree that use of the
|
||||||
|
Covered Code, or any portion thereof, is at Your sole and entire risk. THE
|
||||||
|
COVERED CODE IS PROVIDED "AS IS" AND WITHOUT WARRANTY, UPGRADES OR SUPPORT OF
|
||||||
|
ANY KIND AND APPLE AND APPLE'S LICENSOR(S) (COLLECTIVELY REFERRED TO AS "APPLE"
|
||||||
|
FOR THE PURPOSES OF SECTIONS 8 AND 9) AND ALL CONTRIBUTORS EXPRESSLY DISCLAIM
|
||||||
|
ALL WARRANTIES AND/OR CONDITIONS, EXPRESS OR IMPLIED, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES AND/OR CONDITIONS OF MERCHANTABILITY, OF
|
||||||
|
SATISFACTORY QUALITY, OF FITNESS FOR A PARTICULAR PURPOSE, OF ACCURACY, OF
|
||||||
|
QUIET ENJOYMENT, AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. APPLE AND EACH
|
||||||
|
CONTRIBUTOR DOES NOT WARRANT AGAINST INTERFERENCE WITH YOUR ENJOYMENT OF THE
|
||||||
|
COVERED CODE, THAT THE FUNCTIONS CONTAINED IN THE COVERED CODE WILL MEET YOUR
|
||||||
|
REQUIREMENTS, THAT THE OPERATION OF THE COVERED CODE WILL BE UNINTERRUPTED OR
|
||||||
|
ERROR-FREE, OR THAT DEFECTS IN THE COVERED CODE WILL BE CORRECTED. NO ORAL OR
|
||||||
|
WRITTEN INFORMATION OR ADVICE GIVEN BY APPLE, AN APPLE AUTHORIZED
|
||||||
|
REPRESENTATIVE OR ANY CONTRIBUTOR SHALL CREATE A WARRANTY. You acknowledge
|
||||||
|
that the Covered Code is not intended for use in the operation of nuclear
|
||||||
|
facilities, aircraft navigation, communication systems, or air traffic control
|
||||||
|
machines in which case the failure of the Covered Code could lead to death,
|
||||||
|
personal injury, or severe physical or environmental damage.
|
||||||
|
|
||||||
|
9. LIMITATION OF LIABILITY. TO THE EXTENT NOT PROHIBITED BY LAW, IN NO
|
||||||
|
EVENT SHALL APPLE OR ANY CONTRIBUTOR BE LIABLE FOR ANY INCIDENTAL, SPECIAL,
|
||||||
|
INDIRECT OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATING TO THIS LICENSE OR
|
||||||
|
YOUR USE OR INABILITY TO USE THE COVERED CODE, OR ANY PORTION THEREOF, WHETHER
|
||||||
|
UNDER A THEORY OF CONTRACT, WARRANTY, TORT (INCLUDING NEGLIGENCE), PRODUCTS
|
||||||
|
LIABILITY OR OTHERWISE, EVEN IF APPLE OR SUCH CONTRIBUTOR HAS BEEN ADVISED OF
|
||||||
|
THE POSSIBILITY OF SUCH DAMAGES AND NOTWITHSTANDING THE FAILURE OF ESSENTIAL
|
||||||
|
PURPOSE OF ANY REMEDY. SOME JURISDICTIONS DO NOT ALLOW THE LIMITATION OF
|
||||||
|
LIABILITY OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS LIMITATION MAY NOT
|
||||||
|
APPLY TO YOU. In no event shall Apple's total liability to You for all damages
|
||||||
|
(other than as may be required by applicable law) under this License exceed the
|
||||||
|
amount of fifty dollars ($50.00).
|
||||||
|
|
||||||
|
10. Trademarks. This License does not grant any rights to use the
|
||||||
|
trademarks or trade names "Apple", "Mac", "Mac OS", "QuickTime", "QuickTime
|
||||||
|
Streaming Server" or any other trademarks, service marks, logos or trade names
|
||||||
|
belonging to Apple (collectively "Apple Marks") or to any trademark, service
|
||||||
|
mark, logo or trade name belonging to any Contributor. You agree not to use
|
||||||
|
any Apple Marks in or as part of the name of products derived from the Original
|
||||||
|
Code or to endorse or promote products derived from the Original Code other
|
||||||
|
than as expressly permitted by and in strict compliance at all times with
|
||||||
|
Apple's third party trademark usage guidelines which are posted at
|
||||||
|
http://www.apple.com/legal/guidelinesfor3rdparties.html.
|
||||||
|
|
||||||
|
11. Ownership. Subject to the licenses granted under this License, each
|
||||||
|
Contributor retains all rights, title and interest in and to any Modifications
|
||||||
|
made by such Contributor. Apple retains all rights, title and interest in and
|
||||||
|
to the Original Code and any Modifications made by or on behalf of Apple
|
||||||
|
("Apple Modifications"), and such Apple Modifications will not be automatically
|
||||||
|
subject to this License. Apple may, at its sole discretion, choose to license
|
||||||
|
such Apple Modifications under this License, or on different terms from those
|
||||||
|
contained in this License or may choose not to license them at all.
|
||||||
|
|
||||||
|
12. Termination.
|
||||||
|
|
||||||
|
12.1 Termination. This License and the rights granted hereunder will
|
||||||
|
terminate:
|
||||||
|
|
||||||
|
(a) automatically without notice from Apple if You fail to comply with any
|
||||||
|
term(s) of this License and fail to cure such breach within 30 days of becoming
|
||||||
|
aware of such breach;
|
||||||
|
(b) immediately in the event of the circumstances described in Section
|
||||||
|
13.5(b); or
|
||||||
|
(c) automatically without notice from Apple if You, at any time during the
|
||||||
|
term of this License, commence an action for patent infringement against Apple;
|
||||||
|
provided that Apple did not first commence an action for patent infringement
|
||||||
|
against You in that instance.
|
||||||
|
|
||||||
|
12.2 Effect of Termination. Upon termination, You agree to immediately stop
|
||||||
|
any further use, reproduction, modification, sublicensing and distribution of
|
||||||
|
the Covered Code. All sublicenses to the Covered Code which have been properly
|
||||||
|
granted prior to termination shall survive any termination of this License.
|
||||||
|
Provisions which, by their nature, should remain in effect beyond the
|
||||||
|
termination of this License shall survive, including but not limited to
|
||||||
|
Sections 3, 5, 8, 9, 10, 11, 12.2 and 13. No party will be liable to any other
|
||||||
|
for compensation, indemnity or damages of any sort solely as a result of
|
||||||
|
terminating this License in accordance with its terms, and termination of this
|
||||||
|
License will be without prejudice to any other right or remedy of any party.
|
||||||
|
|
||||||
|
13. Miscellaneous.
|
||||||
|
|
||||||
|
13.1 Government End Users. The Covered Code is a "commercial item" as
|
||||||
|
defined in FAR 2.101. Government software and technical data rights in the
|
||||||
|
Covered Code include only those rights customarily provided to the public as
|
||||||
|
defined in this License. This customary commercial license in technical data
|
||||||
|
and software is provided in accordance with FAR 12.211 (Technical Data) and
|
||||||
|
12.212 (Computer Software) and, for Department of Defense purchases, DFAR
|
||||||
|
252.227-7015 (Technical Data -- Commercial Items) and 227.7202-3 (Rights in
|
||||||
|
Commercial Computer Software or Computer Software Documentation). Accordingly,
|
||||||
|
all U.S. Government End Users acquire Covered Code with only those rights set
|
||||||
|
forth herein.
|
||||||
|
|
||||||
|
13.2 Relationship of Parties. This License will not be construed as
|
||||||
|
creating an agency, partnership, joint venture or any other form of legal
|
||||||
|
association between or among You, Apple or any Contributor, and You will not
|
||||||
|
represent to the contrary, whether expressly, by implication, appearance or
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
13.3 Independent Development. Nothing in this License will impair Apple's
|
||||||
|
right to acquire, license, develop, have others develop for it, market and/or
|
||||||
|
distribute technology or products that perform the same or similar functions
|
||||||
|
as, or otherwise compete with, Modifications, Larger Works, technology or
|
||||||
|
products that You may develop, produce, market or distribute.
|
||||||
|
|
||||||
|
13.4 Waiver; Construction. Failure by Apple or any Contributor to enforce
|
||||||
|
any provision of this License will not be deemed a waiver of future enforcement
|
||||||
|
of that or any other provision. Any law or regulation which provides that the
|
||||||
|
language of a contract shall be construed against the drafter will not apply to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
13.5 Severability. (a) If for any reason a court of competent jurisdiction
|
||||||
|
finds any provision of this License, or portion thereof, to be unenforceable,
|
||||||
|
that provision of the License will be enforced to the maximum extent
|
||||||
|
permissible so as to effect the economic benefits and intent of the parties,
|
||||||
|
and the remainder of this License will continue in full force and effect. (b)
|
||||||
|
Notwithstanding the foregoing, if applicable law prohibits or restricts You
|
||||||
|
from fully and/or specifically complying with Sections 2 and/or 3 or prevents
|
||||||
|
the enforceability of either of those Sections, this License will immediately
|
||||||
|
terminate and You must immediately discontinue any use of the Covered Code and
|
||||||
|
destroy all copies of it that are in your possession or control.
|
||||||
|
|
||||||
|
13.6 Dispute Resolution. Any litigation or other dispute resolution between
|
||||||
|
You and Apple relating to this License shall take place in the Northern
|
||||||
|
District of California, and You and Apple hereby consent to the personal
|
||||||
|
jurisdiction of, and venue in, the state and federal courts within that
|
||||||
|
District with respect to this License. The application of the United Nations
|
||||||
|
Convention on Contracts for the International Sale of Goods is expressly
|
||||||
|
excluded.
|
||||||
|
|
||||||
|
13.7 Entire Agreement; Governing Law. This License constitutes the entire
|
||||||
|
agreement between the parties with respect to the subject matter hereof. This
|
||||||
|
License shall be governed by the laws of the United States and the State of
|
||||||
|
California, except that body of California law concerning conflicts of law.
|
||||||
|
|
||||||
|
Where You are located in the province of Quebec, Canada, the following clause
|
||||||
|
applies: The parties hereby confirm that they have requested that this License
|
||||||
|
and all related documents be drafted in English. Les parties ont exigé que le
|
||||||
|
présent contrat et tous les documents connexes soient rédigés en anglais.
|
||||||
|
|
||||||
|
EXHIBIT A.
|
||||||
|
|
||||||
|
"Portions Copyright (c) 1999-2007 Apple Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
This file contains Original Code and/or Modifications of Original Code as
|
||||||
|
defined in and that are subject to the Apple Public Source License Version 2.0
|
||||||
|
(the 'License'). You may not use this file except in compliance with the
|
||||||
|
License. Please obtain a copy of the License at
|
||||||
|
http://www.opensource.apple.com/apsl/ and read it before using this file.
|
||||||
|
|
||||||
|
The Original Code and all software distributed under the License are
|
||||||
|
distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS
|
||||||
|
OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT
|
||||||
|
LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. Please see the License for the
|
||||||
|
specific language governing rights and limitations under the License."
|
||||||
|
|
362
alac/codec/ag_dec.c
Normal file
362
alac/codec/ag_dec.c
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
File: ag_dec.c
|
||||||
|
|
||||||
|
Contains: Adaptive Golomb decode routines.
|
||||||
|
|
||||||
|
Copyright: (c) 2001-2011 Apple, Inc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aglib.h"
|
||||||
|
#include "ALACBitUtilities.h"
|
||||||
|
#include "ALACAudioTypes.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#if __GNUC__ && TARGET_OS_MAC
|
||||||
|
#if __POWERPC__
|
||||||
|
#include <ppc_intrinsics.h>
|
||||||
|
#else
|
||||||
|
#include <libkern/OSByteOrder.h>
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define CODE_TO_LONG_MAXBITS 32
|
||||||
|
#define N_MAX_MEAN_CLAMP 0xffff
|
||||||
|
#define N_MEAN_CLAMP_VAL 0xffff
|
||||||
|
#define REPORT_VAL 40
|
||||||
|
|
||||||
|
#if __GNUC__
|
||||||
|
#define ALWAYS_INLINE __attribute__((always_inline))
|
||||||
|
#else
|
||||||
|
#define ALWAYS_INLINE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* And on the subject of the CodeWarrior x86 compiler and inlining, I reworked a lot of this
|
||||||
|
to help the compiler out. In many cases this required manual inlining or a macro. Sorry
|
||||||
|
if it is ugly but the performance gains are well worth it.
|
||||||
|
- WSK 5/19/04
|
||||||
|
*/
|
||||||
|
|
||||||
|
void set_standard_ag_params(AGParamRecPtr params, uint32_t fullwidth, uint32_t sectorwidth)
|
||||||
|
{
|
||||||
|
/* Use
|
||||||
|
fullwidth = sectorwidth = numOfSamples, for analog 1-dimensional type-short data,
|
||||||
|
but use
|
||||||
|
fullwidth = full image width, sectorwidth = sector (patch) width
|
||||||
|
for such as image (2-dim.) data.
|
||||||
|
*/
|
||||||
|
set_ag_params( params, MB0, PB0, KB0, fullwidth, sectorwidth, MAX_RUN_DEFAULT );
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_ag_params(AGParamRecPtr params, uint32_t m, uint32_t p, uint32_t k, uint32_t f, uint32_t s, uint32_t maxrun)
|
||||||
|
{
|
||||||
|
params->mb = params->mb0 = m;
|
||||||
|
params->pb = p;
|
||||||
|
params->kb = k;
|
||||||
|
params->wb = (1u<<params->kb)-1;
|
||||||
|
params->qb = QB-params->pb;
|
||||||
|
params->fw = f;
|
||||||
|
params->sw = s;
|
||||||
|
params->maxrun = maxrun;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if PRAGMA_MARK
|
||||||
|
#pragma mark -
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// note: implementing this with some kind of "count leading zeros" assembly is a big performance win
|
||||||
|
static inline int32_t lead( int32_t m )
|
||||||
|
{
|
||||||
|
long j;
|
||||||
|
unsigned long c = (1ul << 31);
|
||||||
|
|
||||||
|
for(j=0; j < 32; j++)
|
||||||
|
{
|
||||||
|
if((c & m) != 0)
|
||||||
|
break;
|
||||||
|
c >>= 1;
|
||||||
|
}
|
||||||
|
return (j);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define arithmin(a, b) ((a) < (b) ? (a) : (b))
|
||||||
|
|
||||||
|
static inline int32_t ALWAYS_INLINE lg3a( int32_t x)
|
||||||
|
{
|
||||||
|
int32_t result;
|
||||||
|
|
||||||
|
x += 3;
|
||||||
|
result = lead(x);
|
||||||
|
|
||||||
|
return 31 - result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t ALWAYS_INLINE read32bit( uint8_t * buffer )
|
||||||
|
{
|
||||||
|
// embedded CPUs typically can't read unaligned 32-bit words so just read the bytes
|
||||||
|
uint32_t value;
|
||||||
|
|
||||||
|
value = ((uint32_t)buffer[0] << 24) | ((uint32_t)buffer[1] << 16) |
|
||||||
|
((uint32_t)buffer[2] << 8) | (uint32_t)buffer[3];
|
||||||
|
return value;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if PRAGMA_MARK
|
||||||
|
#pragma mark -
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define get_next_fromlong(inlong, suff) ((inlong) >> (32 - (suff)))
|
||||||
|
|
||||||
|
|
||||||
|
static inline uint32_t ALWAYS_INLINE
|
||||||
|
getstreambits( uint8_t *in, int32_t bitoffset, int32_t numbits )
|
||||||
|
{
|
||||||
|
uint32_t load1, load2;
|
||||||
|
uint32_t byteoffset = bitoffset / 8;
|
||||||
|
uint32_t result;
|
||||||
|
|
||||||
|
//Assert( numbits <= 32 );
|
||||||
|
|
||||||
|
load1 = read32bit( in + byteoffset );
|
||||||
|
|
||||||
|
if ( (numbits + (bitoffset & 0x7)) > 32)
|
||||||
|
{
|
||||||
|
int32_t load2shift;
|
||||||
|
|
||||||
|
result = load1 << (bitoffset & 0x7);
|
||||||
|
load2 = (uint32_t) in[byteoffset+4];
|
||||||
|
load2shift = (8-(numbits + (bitoffset & 0x7)-32));
|
||||||
|
load2 >>= load2shift;
|
||||||
|
result >>= (32-numbits);
|
||||||
|
result |= load2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = load1 >> (32-numbits-(bitoffset & 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
// a shift of >= "the number of bits in the type of the value being shifted" results in undefined
|
||||||
|
// behavior so don't try to shift by 32
|
||||||
|
if ( numbits != (sizeof(result) * 8) )
|
||||||
|
result &= ~(0xfffffffful << numbits);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline int32_t dyn_get(unsigned char *in, uint32_t *bitPos, uint32_t m, uint32_t k)
|
||||||
|
{
|
||||||
|
uint32_t tempbits = *bitPos;
|
||||||
|
uint32_t result;
|
||||||
|
uint32_t pre = 0, v;
|
||||||
|
uint32_t streamlong;
|
||||||
|
|
||||||
|
streamlong = read32bit( in + (tempbits >> 3) );
|
||||||
|
streamlong <<= (tempbits & 7);
|
||||||
|
|
||||||
|
/* find the number of bits in the prefix */
|
||||||
|
{
|
||||||
|
uint32_t notI = ~streamlong;
|
||||||
|
pre = lead( notI);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pre >= MAX_PREFIX_16)
|
||||||
|
{
|
||||||
|
pre = MAX_PREFIX_16;
|
||||||
|
tempbits += pre;
|
||||||
|
streamlong <<= pre;
|
||||||
|
result = get_next_fromlong(streamlong,MAX_DATATYPE_BITS_16);
|
||||||
|
tempbits += MAX_DATATYPE_BITS_16;
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// all of the bits must fit within the long we have loaded
|
||||||
|
//Assert(pre+1+k <= 32);
|
||||||
|
|
||||||
|
tempbits += pre;
|
||||||
|
tempbits += 1;
|
||||||
|
streamlong <<= pre+1;
|
||||||
|
v = get_next_fromlong(streamlong, k);
|
||||||
|
tempbits += k;
|
||||||
|
|
||||||
|
result = pre*m + v-1;
|
||||||
|
|
||||||
|
if(v<2) {
|
||||||
|
result -= (v-1);
|
||||||
|
tempbits -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*bitPos = tempbits;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline int32_t dyn_get_32bit( uint8_t * in, uint32_t * bitPos, int32_t m, int32_t k, int32_t maxbits )
|
||||||
|
{
|
||||||
|
uint32_t tempbits = *bitPos;
|
||||||
|
uint32_t v;
|
||||||
|
uint32_t streamlong;
|
||||||
|
uint32_t result;
|
||||||
|
|
||||||
|
streamlong = read32bit( in + (tempbits >> 3) );
|
||||||
|
streamlong <<= (tempbits & 7);
|
||||||
|
|
||||||
|
/* find the number of bits in the prefix */
|
||||||
|
{
|
||||||
|
uint32_t notI = ~streamlong;
|
||||||
|
result = lead( notI);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result >= MAX_PREFIX_32)
|
||||||
|
{
|
||||||
|
result = getstreambits(in, tempbits+MAX_PREFIX_32, maxbits);
|
||||||
|
tempbits += MAX_PREFIX_32 + maxbits;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* all of the bits must fit within the long we have loaded*/
|
||||||
|
//Assert(k<=14);
|
||||||
|
//Assert(result<MAX_PREFIX_32);
|
||||||
|
//Assert(result+1+k <= 32);
|
||||||
|
|
||||||
|
tempbits += result;
|
||||||
|
tempbits += 1;
|
||||||
|
|
||||||
|
if (k != 1)
|
||||||
|
{
|
||||||
|
streamlong <<= result+1;
|
||||||
|
v = get_next_fromlong(streamlong, k);
|
||||||
|
tempbits += k;
|
||||||
|
tempbits -= 1;
|
||||||
|
result = result*m;
|
||||||
|
|
||||||
|
if(v>=2)
|
||||||
|
{
|
||||||
|
result += (v-1);
|
||||||
|
tempbits += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*bitPos = tempbits;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t dyn_decomp( AGParamRecPtr params, BitBuffer * bitstream, int32_t * pc, int32_t numSamples, int32_t maxSize, uint32_t * outNumBits )
|
||||||
|
{
|
||||||
|
uint8_t *in;
|
||||||
|
int32_t *outPtr = pc;
|
||||||
|
uint32_t bitPos, startPos, maxPos;
|
||||||
|
uint32_t j, m, k, n, c, mz;
|
||||||
|
int32_t del, zmode;
|
||||||
|
uint32_t mb;
|
||||||
|
uint32_t pb_local = params->pb;
|
||||||
|
uint32_t kb_local = params->kb;
|
||||||
|
uint32_t wb_local = params->wb;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
RequireAction( (bitstream != nil) && (pc != nil) && (outNumBits != nil), return kALAC_ParamError; );
|
||||||
|
*outNumBits = 0;
|
||||||
|
|
||||||
|
in = bitstream->cur;
|
||||||
|
startPos = bitstream->bitIndex;
|
||||||
|
maxPos = bitstream->byteSize * 8;
|
||||||
|
bitPos = startPos;
|
||||||
|
|
||||||
|
mb = params->mb0;
|
||||||
|
zmode = 0;
|
||||||
|
|
||||||
|
c = 0;
|
||||||
|
status = ALAC_noErr;
|
||||||
|
|
||||||
|
while (c < numSamples)
|
||||||
|
{
|
||||||
|
// bail if we've run off the end of the buffer
|
||||||
|
RequireAction( bitPos < maxPos, status = kALAC_ParamError; goto Exit; );
|
||||||
|
|
||||||
|
m = (mb)>>QBSHIFT;
|
||||||
|
k = lg3a(m);
|
||||||
|
|
||||||
|
k = arithmin(k, kb_local);
|
||||||
|
m = (1<<k)-1;
|
||||||
|
|
||||||
|
n = dyn_get_32bit( in, &bitPos, m, k, maxSize );
|
||||||
|
|
||||||
|
// least significant bit is sign bit
|
||||||
|
{
|
||||||
|
uint32_t ndecode = n + zmode;
|
||||||
|
int32_t multiplier = (- (ndecode&1));
|
||||||
|
|
||||||
|
multiplier |= 1;
|
||||||
|
del = ((ndecode+1) >> 1) * (multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
*outPtr++ = del;
|
||||||
|
|
||||||
|
c++;
|
||||||
|
|
||||||
|
mb = pb_local*(n+zmode) + mb - ((pb_local*mb)>>QBSHIFT);
|
||||||
|
|
||||||
|
// update mean tracking
|
||||||
|
if (n > N_MAX_MEAN_CLAMP)
|
||||||
|
mb = N_MEAN_CLAMP_VAL;
|
||||||
|
|
||||||
|
zmode = 0;
|
||||||
|
|
||||||
|
if (((mb << MMULSHIFT) < QB) && (c < numSamples))
|
||||||
|
{
|
||||||
|
zmode = 1;
|
||||||
|
k = lead(mb) - BITOFF+((mb+MOFF)>>MDENSHIFT);
|
||||||
|
mz = ((1<<k)-1) & wb_local;
|
||||||
|
|
||||||
|
n = dyn_get(in, &bitPos, mz, k);
|
||||||
|
|
||||||
|
RequireAction(c+n <= numSamples, status = kALAC_ParamError; goto Exit; );
|
||||||
|
|
||||||
|
for(j=0; j < n; j++)
|
||||||
|
{
|
||||||
|
*outPtr++ = 0;
|
||||||
|
++c;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(n >= 65535)
|
||||||
|
zmode = 0;
|
||||||
|
|
||||||
|
mb = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Exit:
|
||||||
|
*outNumBits = (bitPos - startPos);
|
||||||
|
BitBufferAdvance( bitstream, *outNumBits );
|
||||||
|
RequireAction( bitstream->cur <= bitstream->end, status = kALAC_ParamError; );
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
370
alac/codec/ag_enc.c
Normal file
370
alac/codec/ag_enc.c
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
File: ag_enc.c
|
||||||
|
|
||||||
|
Contains: Adaptive Golomb encode routines.
|
||||||
|
|
||||||
|
Copyright: (c) 2001-2011 Apple, Inc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "aglib.h"
|
||||||
|
#include "ALACBitUtilities.h"
|
||||||
|
#include "EndianPortable.h"
|
||||||
|
#include "ALACAudioTypes.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#if __GNUC__ && TARGET_OS_MAC
|
||||||
|
#if __POWERPC__
|
||||||
|
#include <ppc_intrinsics.h>
|
||||||
|
#else
|
||||||
|
#include <libkern/OSByteOrder.h>
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define CODE_TO_LONG_MAXBITS 32
|
||||||
|
#define N_MAX_MEAN_CLAMP 0xffff
|
||||||
|
#define N_MEAN_CLAMP_VAL 0xffff
|
||||||
|
#define REPORT_VAL 40
|
||||||
|
|
||||||
|
#if __GNUC__
|
||||||
|
#define ALWAYS_INLINE __attribute__((always_inline))
|
||||||
|
#else
|
||||||
|
#define ALWAYS_INLINE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/* And on the subject of the CodeWarrior x86 compiler and inlining, I reworked a lot of this
|
||||||
|
to help the compiler out. In many cases this required manual inlining or a macro. Sorry
|
||||||
|
if it is ugly but the performance gains are well worth it.
|
||||||
|
- WSK 5/19/04
|
||||||
|
*/
|
||||||
|
|
||||||
|
// note: implementing this with some kind of "count leading zeros" assembly is a big performance win
|
||||||
|
static inline int32_t lead( int32_t m )
|
||||||
|
{
|
||||||
|
long j;
|
||||||
|
unsigned long c = (1ul << 31);
|
||||||
|
|
||||||
|
for(j=0; j < 32; j++)
|
||||||
|
{
|
||||||
|
if((c & m) != 0)
|
||||||
|
break;
|
||||||
|
c >>= 1;
|
||||||
|
}
|
||||||
|
return (j);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define arithmin(a, b) ((a) < (b) ? (a) : (b))
|
||||||
|
|
||||||
|
static inline int32_t ALWAYS_INLINE lg3a( int32_t x)
|
||||||
|
{
|
||||||
|
int32_t result;
|
||||||
|
|
||||||
|
x += 3;
|
||||||
|
result = lead(x);
|
||||||
|
|
||||||
|
return 31 - result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int32_t ALWAYS_INLINE abs_func( int32_t a )
|
||||||
|
{
|
||||||
|
// note: the CW PPC intrinsic __abs() turns into these instructions so no need to try and use it
|
||||||
|
int32_t isneg = a >> 31;
|
||||||
|
int32_t xorval = a ^ isneg;
|
||||||
|
int32_t result = xorval-isneg;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t ALWAYS_INLINE read32bit( uint8_t * buffer )
|
||||||
|
{
|
||||||
|
// embedded CPUs typically can't read unaligned 32-bit words so just read the bytes
|
||||||
|
uint32_t value;
|
||||||
|
|
||||||
|
value = ((uint32_t)buffer[0] << 24) | ((uint32_t)buffer[1] << 16) |
|
||||||
|
((uint32_t)buffer[2] << 8) | (uint32_t)buffer[3];
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if PRAGMA_MARK
|
||||||
|
#pragma mark -
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static inline int32_t dyn_code(int32_t m, int32_t k, int32_t n, uint32_t *outNumBits)
|
||||||
|
{
|
||||||
|
uint32_t div, mod, de;
|
||||||
|
uint32_t numBits;
|
||||||
|
uint32_t value;
|
||||||
|
|
||||||
|
//Assert( n >= 0 );
|
||||||
|
|
||||||
|
div = n/m;
|
||||||
|
|
||||||
|
if(div >= MAX_PREFIX_16)
|
||||||
|
{
|
||||||
|
numBits = MAX_PREFIX_16 + MAX_DATATYPE_BITS_16;
|
||||||
|
value = (((1<<MAX_PREFIX_16)-1)<<MAX_DATATYPE_BITS_16) + n;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mod = n%m;
|
||||||
|
de = (mod == 0);
|
||||||
|
numBits = div + k + 1 - de;
|
||||||
|
value = (((1<<div)-1)<<(numBits-div)) + mod + 1 - de;
|
||||||
|
|
||||||
|
// if coding this way is bigger than doing escape, then do escape
|
||||||
|
if (numBits > MAX_PREFIX_16 + MAX_DATATYPE_BITS_16)
|
||||||
|
{
|
||||||
|
numBits = MAX_PREFIX_16 + MAX_DATATYPE_BITS_16;
|
||||||
|
value = (((1<<MAX_PREFIX_16)-1)<<MAX_DATATYPE_BITS_16) + n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*outNumBits = numBits;
|
||||||
|
|
||||||
|
return (int32_t) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline int32_t dyn_code_32bit(int32_t maxbits, uint32_t m, uint32_t k, uint32_t n, uint32_t *outNumBits, uint32_t *outValue, uint32_t *overflow, uint32_t *overflowbits)
|
||||||
|
{
|
||||||
|
uint32_t div, mod, de;
|
||||||
|
uint32_t numBits;
|
||||||
|
uint32_t value;
|
||||||
|
int32_t didOverflow = 0;
|
||||||
|
|
||||||
|
div = n/m;
|
||||||
|
|
||||||
|
if (div < MAX_PREFIX_32)
|
||||||
|
{
|
||||||
|
mod = n - (m * div);
|
||||||
|
|
||||||
|
de = (mod == 0);
|
||||||
|
numBits = div + k + 1 - de;
|
||||||
|
value = (((1<<div)-1)<<(numBits-div)) + mod + 1 - de;
|
||||||
|
if (numBits > 25)
|
||||||
|
goto codeasescape;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
codeasescape:
|
||||||
|
numBits = MAX_PREFIX_32;
|
||||||
|
value = (((1<<MAX_PREFIX_32)-1));
|
||||||
|
*overflow = n;
|
||||||
|
*overflowbits = maxbits;
|
||||||
|
didOverflow = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*outNumBits = numBits;
|
||||||
|
*outValue = value;
|
||||||
|
|
||||||
|
return didOverflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline void ALWAYS_INLINE dyn_jam_noDeref(unsigned char *out, uint32_t bitPos, uint32_t numBits, uint32_t value)
|
||||||
|
{
|
||||||
|
uint32_t *i = (uint32_t *)(out + (bitPos >> 3));
|
||||||
|
uint32_t mask;
|
||||||
|
uint32_t curr;
|
||||||
|
uint32_t shift;
|
||||||
|
|
||||||
|
//Assert( numBits <= 32 );
|
||||||
|
|
||||||
|
curr = *i;
|
||||||
|
curr = Swap32NtoB( curr );
|
||||||
|
|
||||||
|
shift = 32 - (bitPos & 7) - numBits;
|
||||||
|
|
||||||
|
mask = ~0u >> (32 - numBits); // mask must be created in two steps to avoid compiler sequencing ambiguity
|
||||||
|
mask <<= shift;
|
||||||
|
|
||||||
|
value = (value << shift) & mask;
|
||||||
|
value |= curr & ~mask;
|
||||||
|
|
||||||
|
*i = Swap32BtoN( value );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline void ALWAYS_INLINE dyn_jam_noDeref_large(unsigned char *out, uint32_t bitPos, uint32_t numBits, uint32_t value)
|
||||||
|
{
|
||||||
|
uint32_t * i = (uint32_t *)(out + (bitPos>>3));
|
||||||
|
uint32_t w;
|
||||||
|
uint32_t curr;
|
||||||
|
uint32_t mask;
|
||||||
|
int32_t shiftvalue = (32 - (bitPos&7) - numBits);
|
||||||
|
|
||||||
|
//Assert(numBits <= 32);
|
||||||
|
|
||||||
|
curr = *i;
|
||||||
|
curr = Swap32NtoB( curr );
|
||||||
|
|
||||||
|
if (shiftvalue < 0)
|
||||||
|
{
|
||||||
|
uint8_t tailbyte;
|
||||||
|
uint8_t *tailptr;
|
||||||
|
|
||||||
|
w = value >> -shiftvalue;
|
||||||
|
mask = ~0u >> -shiftvalue;
|
||||||
|
w |= (curr & ~mask);
|
||||||
|
|
||||||
|
tailptr = ((uint8_t *)i) + 4;
|
||||||
|
tailbyte = (value << ((8+shiftvalue))) & 0xff;
|
||||||
|
*tailptr = (uint8_t)tailbyte;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mask = ~0u >> (32 - numBits);
|
||||||
|
mask <<= shiftvalue; // mask must be created in two steps to avoid compiler sequencing ambiguity
|
||||||
|
|
||||||
|
w = (value << shiftvalue) & mask;
|
||||||
|
w |= curr & ~mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
*i = Swap32BtoN( w );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int32_t dyn_comp( AGParamRecPtr params, int32_t * pc, BitBuffer * bitstream, int32_t numSamples, int32_t bitSize, uint32_t * outNumBits )
|
||||||
|
{
|
||||||
|
unsigned char * out;
|
||||||
|
uint32_t bitPos, startPos;
|
||||||
|
uint32_t m, k, n, c, mz, nz;
|
||||||
|
uint32_t numBits;
|
||||||
|
uint32_t value;
|
||||||
|
int32_t del, zmode;
|
||||||
|
uint32_t overflow, overflowbits;
|
||||||
|
int32_t status;
|
||||||
|
|
||||||
|
// shadow the variables in params so there's not the dereferencing overhead
|
||||||
|
uint32_t mb, pb, kb, wb;
|
||||||
|
int32_t rowPos = 0;
|
||||||
|
int32_t rowSize = params->sw;
|
||||||
|
int32_t rowJump = (params->fw) - rowSize;
|
||||||
|
int32_t * inPtr = pc;
|
||||||
|
|
||||||
|
*outNumBits = 0;
|
||||||
|
RequireAction( (bitSize >= 1) && (bitSize <= 32), return kALAC_ParamError; );
|
||||||
|
|
||||||
|
out = bitstream->cur;
|
||||||
|
startPos = bitstream->bitIndex;
|
||||||
|
bitPos = startPos;
|
||||||
|
|
||||||
|
mb = params->mb = params->mb0;
|
||||||
|
pb = params->pb;
|
||||||
|
kb = params->kb;
|
||||||
|
wb = params->wb;
|
||||||
|
zmode = 0;
|
||||||
|
|
||||||
|
c=0;
|
||||||
|
status = ALAC_noErr;
|
||||||
|
|
||||||
|
while (c < numSamples)
|
||||||
|
{
|
||||||
|
m = mb >> QBSHIFT;
|
||||||
|
k = lg3a(m);
|
||||||
|
if ( k > kb)
|
||||||
|
{
|
||||||
|
k = kb;
|
||||||
|
}
|
||||||
|
m = (1<<k)-1;
|
||||||
|
|
||||||
|
del = *inPtr++;
|
||||||
|
rowPos++;
|
||||||
|
|
||||||
|
n = (abs_func(del) << 1) - ((del >> 31) & 1) - zmode;
|
||||||
|
//Assert( 32-lead(n) <= bitSize );
|
||||||
|
|
||||||
|
if ( dyn_code_32bit(bitSize, m, k, n, &numBits, &value, &overflow, &overflowbits) )
|
||||||
|
{
|
||||||
|
dyn_jam_noDeref(out, bitPos, numBits, value);
|
||||||
|
bitPos += numBits;
|
||||||
|
dyn_jam_noDeref_large(out, bitPos, overflowbits, overflow);
|
||||||
|
bitPos += overflowbits;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dyn_jam_noDeref(out, bitPos, numBits, value);
|
||||||
|
bitPos += numBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
c++;
|
||||||
|
if ( rowPos >= rowSize)
|
||||||
|
{
|
||||||
|
rowPos = 0;
|
||||||
|
inPtr += rowJump;
|
||||||
|
}
|
||||||
|
|
||||||
|
mb = pb * (n + zmode) + mb - ((pb *mb)>>QBSHIFT);
|
||||||
|
|
||||||
|
// update mean tracking if it's overflowed
|
||||||
|
if (n > N_MAX_MEAN_CLAMP)
|
||||||
|
mb = N_MEAN_CLAMP_VAL;
|
||||||
|
|
||||||
|
zmode = 0;
|
||||||
|
|
||||||
|
RequireAction(c <= numSamples, status = kALAC_ParamError; goto Exit; );
|
||||||
|
|
||||||
|
if (((mb << MMULSHIFT) < QB) && (c < numSamples))
|
||||||
|
{
|
||||||
|
zmode = 1;
|
||||||
|
nz = 0;
|
||||||
|
|
||||||
|
while(c<numSamples && *inPtr == 0)
|
||||||
|
{
|
||||||
|
/* Take care of wrap-around globals. */
|
||||||
|
++inPtr;
|
||||||
|
++nz;
|
||||||
|
++c;
|
||||||
|
if ( ++rowPos >= rowSize)
|
||||||
|
{
|
||||||
|
rowPos = 0;
|
||||||
|
inPtr += rowJump;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(nz >= 65535)
|
||||||
|
{
|
||||||
|
zmode = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
k = lead(mb) - BITOFF+((mb+MOFF)>>MDENSHIFT);
|
||||||
|
mz = ((1<<k)-1) & wb;
|
||||||
|
|
||||||
|
value = dyn_code(mz, k, nz, &numBits);
|
||||||
|
dyn_jam_noDeref(out, bitPos, numBits, value);
|
||||||
|
bitPos += numBits;
|
||||||
|
|
||||||
|
mb = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*outNumBits = (bitPos - startPos);
|
||||||
|
BitBufferAdvance( bitstream, *outNumBits );
|
||||||
|
|
||||||
|
Exit:
|
||||||
|
return status;
|
||||||
|
}
|
81
alac/codec/aglib.h
Normal file
81
alac/codec/aglib.h
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
File: aglib.h
|
||||||
|
|
||||||
|
Copyright: (C) 2001-2011 Apple, Inc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef AGLIB_H
|
||||||
|
#define AGLIB_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define QBSHIFT 9
|
||||||
|
#define QB (1<<QBSHIFT)
|
||||||
|
#define PB0 40
|
||||||
|
#define MB0 10
|
||||||
|
#define KB0 14
|
||||||
|
#define MAX_RUN_DEFAULT 255
|
||||||
|
|
||||||
|
#define MMULSHIFT 2
|
||||||
|
#define MDENSHIFT (QBSHIFT - MMULSHIFT - 1)
|
||||||
|
#define MOFF ((1<<(MDENSHIFT-2)))
|
||||||
|
|
||||||
|
#define BITOFF 24
|
||||||
|
|
||||||
|
/* Max. prefix of 1's. */
|
||||||
|
#define MAX_PREFIX_16 9
|
||||||
|
#define MAX_PREFIX_TOLONG_16 15
|
||||||
|
#define MAX_PREFIX_32 9
|
||||||
|
|
||||||
|
/* Max. bits in 16-bit data type */
|
||||||
|
#define MAX_DATATYPE_BITS_16 16
|
||||||
|
|
||||||
|
typedef struct AGParamRec
|
||||||
|
{
|
||||||
|
uint32_t mb, mb0, pb, kb, wb, qb;
|
||||||
|
uint32_t fw, sw;
|
||||||
|
|
||||||
|
uint32_t maxrun;
|
||||||
|
|
||||||
|
// fw = 1, sw = 1;
|
||||||
|
|
||||||
|
} AGParamRec, *AGParamRecPtr;
|
||||||
|
|
||||||
|
struct BitBuffer;
|
||||||
|
|
||||||
|
void set_standard_ag_params(AGParamRecPtr params, uint32_t fullwidth, uint32_t sectorwidth);
|
||||||
|
void set_ag_params(AGParamRecPtr params, uint32_t m, uint32_t p, uint32_t k, uint32_t f, uint32_t s, uint32_t maxrun);
|
||||||
|
|
||||||
|
int32_t dyn_comp(AGParamRecPtr params, int32_t * pc, struct BitBuffer * bitstream, int32_t numSamples, int32_t bitSize, uint32_t * outNumBits);
|
||||||
|
int32_t dyn_decomp(AGParamRecPtr params, struct BitBuffer * bitstream, int32_t * pc, int32_t numSamples, int32_t maxSize, uint32_t * outNumBits);
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif //#ifndef AGLIB_H
|
386
alac/codec/dp_enc.c
Normal file
386
alac/codec/dp_enc.c
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
File: dp_enc.c
|
||||||
|
|
||||||
|
Contains: Dynamic Predictor encode routines
|
||||||
|
|
||||||
|
Copyright: (c) 2001-2011 Apple, Inc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "dplib.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#if __GNUC__
|
||||||
|
#define ALWAYS_INLINE __attribute__((always_inline))
|
||||||
|
#else
|
||||||
|
#define ALWAYS_INLINE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if TARGET_CPU_PPC && (__MWERKS__ >= 0x3200)
|
||||||
|
// align loops to a 16 byte boundary to make the G5 happy
|
||||||
|
#pragma function_align 16
|
||||||
|
#define LOOP_ALIGN asm { align 16 }
|
||||||
|
#else
|
||||||
|
#define LOOP_ALIGN
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void init_coefs( int16_t * coefs, uint32_t denshift, int32_t numPairs )
|
||||||
|
{
|
||||||
|
int32_t k;
|
||||||
|
int32_t den = 1 << denshift;
|
||||||
|
|
||||||
|
coefs[0] = (AINIT * den) >> 4;
|
||||||
|
coefs[1] = (BINIT * den) >> 4;
|
||||||
|
coefs[2] = (CINIT * den) >> 4;
|
||||||
|
for ( k = 3; k < numPairs; k++ )
|
||||||
|
coefs[k] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void copy_coefs( int16_t * srcCoefs, int16_t * dstCoefs, int32_t numPairs )
|
||||||
|
{
|
||||||
|
int32_t k;
|
||||||
|
|
||||||
|
for ( k = 0; k < numPairs; k++ )
|
||||||
|
dstCoefs[k] = srcCoefs[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int32_t ALWAYS_INLINE sign_of_int( int32_t i )
|
||||||
|
{
|
||||||
|
int32_t negishift;
|
||||||
|
|
||||||
|
negishift = ((uint32_t)-i) >> 31;
|
||||||
|
return negishift | (i >> 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pc_block( int32_t * in, int32_t * pc1, int32_t num, int16_t * coefs, int32_t numactive, uint32_t chanbits, uint32_t denshift )
|
||||||
|
{
|
||||||
|
register int16_t a0, a1, a2, a3;
|
||||||
|
register int32_t b0, b1, b2, b3;
|
||||||
|
int32_t j, k, lim;
|
||||||
|
int32_t * pin;
|
||||||
|
int32_t sum1, dd;
|
||||||
|
int32_t sg, sgn;
|
||||||
|
int32_t top;
|
||||||
|
int32_t del, del0;
|
||||||
|
uint32_t chanshift = 32 - chanbits;
|
||||||
|
int32_t denhalf = 1 << (denshift - 1);
|
||||||
|
|
||||||
|
pc1[0] = in[0];
|
||||||
|
if ( numactive == 0 )
|
||||||
|
{
|
||||||
|
// just copy if numactive == 0 (but don't bother if in/out pointers the same)
|
||||||
|
if ( (num > 1) && (in != pc1) )
|
||||||
|
memcpy( &pc1[1], &in[1], (num - 1) * sizeof(int32_t) );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ( numactive == 31 )
|
||||||
|
{
|
||||||
|
// short-circuit if numactive == 31
|
||||||
|
for( j = 1; j < num; j++ )
|
||||||
|
{
|
||||||
|
del = in[j] - in[j-1];
|
||||||
|
pc1[j] = (del << chanshift) >> chanshift;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( j = 1; j <= numactive; j++ )
|
||||||
|
{
|
||||||
|
del = in[j] - in[j-1];
|
||||||
|
pc1[j] = (del << chanshift) >> chanshift;
|
||||||
|
}
|
||||||
|
|
||||||
|
lim = numactive + 1;
|
||||||
|
|
||||||
|
if ( numactive == 4 )
|
||||||
|
{
|
||||||
|
// optimization for numactive == 4
|
||||||
|
a0 = coefs[0];
|
||||||
|
a1 = coefs[1];
|
||||||
|
a2 = coefs[2];
|
||||||
|
a3 = coefs[3];
|
||||||
|
|
||||||
|
for ( j = lim; j < num; j++ )
|
||||||
|
{
|
||||||
|
LOOP_ALIGN
|
||||||
|
|
||||||
|
top = in[j - lim];
|
||||||
|
pin = in + j - 1;
|
||||||
|
|
||||||
|
b0 = top - pin[0];
|
||||||
|
b1 = top - pin[-1];
|
||||||
|
b2 = top - pin[-2];
|
||||||
|
b3 = top - pin[-3];
|
||||||
|
|
||||||
|
sum1 = (denhalf - a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3) >> denshift;
|
||||||
|
|
||||||
|
del = in[j] - top - sum1;
|
||||||
|
del = (del << chanshift) >> chanshift;
|
||||||
|
pc1[j] = del;
|
||||||
|
del0 = del;
|
||||||
|
|
||||||
|
sg = sign_of_int(del);
|
||||||
|
if ( sg > 0 )
|
||||||
|
{
|
||||||
|
sgn = sign_of_int( b3 );
|
||||||
|
a3 -= sgn;
|
||||||
|
del0 -= (4 - 3) * ((sgn * b3) >> denshift);
|
||||||
|
if ( del0 <= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = sign_of_int( b2 );
|
||||||
|
a2 -= sgn;
|
||||||
|
del0 -= (4 - 2) * ((sgn * b2) >> denshift);
|
||||||
|
if ( del0 <= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = sign_of_int( b1 );
|
||||||
|
a1 -= sgn;
|
||||||
|
del0 -= (4 - 1) * ((sgn * b1) >> denshift);
|
||||||
|
if ( del0 <= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
a0 -= sign_of_int( b0 );
|
||||||
|
}
|
||||||
|
else if ( sg < 0 )
|
||||||
|
{
|
||||||
|
// note: to avoid unnecessary negations, we flip the value of "sgn"
|
||||||
|
sgn = -sign_of_int( b3 );
|
||||||
|
a3 -= sgn;
|
||||||
|
del0 -= (4 - 3) * ((sgn * b3) >> denshift);
|
||||||
|
if ( del0 >= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = -sign_of_int( b2 );
|
||||||
|
a2 -= sgn;
|
||||||
|
del0 -= (4 - 2) * ((sgn * b2) >> denshift);
|
||||||
|
if ( del0 >= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = -sign_of_int( b1 );
|
||||||
|
a1 -= sgn;
|
||||||
|
del0 -= (4 - 1) * ((sgn * b1) >> denshift);
|
||||||
|
if ( del0 >= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
a0 += sign_of_int( b0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coefs[0] = a0;
|
||||||
|
coefs[1] = a1;
|
||||||
|
coefs[2] = a2;
|
||||||
|
coefs[3] = a3;
|
||||||
|
}
|
||||||
|
else if ( numactive == 8 )
|
||||||
|
{
|
||||||
|
// optimization for numactive == 8
|
||||||
|
register int16_t a4, a5, a6, a7;
|
||||||
|
register int32_t b4, b5, b6, b7;
|
||||||
|
|
||||||
|
a0 = coefs[0];
|
||||||
|
a1 = coefs[1];
|
||||||
|
a2 = coefs[2];
|
||||||
|
a3 = coefs[3];
|
||||||
|
a4 = coefs[4];
|
||||||
|
a5 = coefs[5];
|
||||||
|
a6 = coefs[6];
|
||||||
|
a7 = coefs[7];
|
||||||
|
|
||||||
|
for ( j = lim; j < num; j++ )
|
||||||
|
{
|
||||||
|
LOOP_ALIGN
|
||||||
|
|
||||||
|
top = in[j - lim];
|
||||||
|
pin = in + j - 1;
|
||||||
|
|
||||||
|
b0 = top - (*pin--);
|
||||||
|
b1 = top - (*pin--);
|
||||||
|
b2 = top - (*pin--);
|
||||||
|
b3 = top - (*pin--);
|
||||||
|
b4 = top - (*pin--);
|
||||||
|
b5 = top - (*pin--);
|
||||||
|
b6 = top - (*pin--);
|
||||||
|
b7 = top - (*pin);
|
||||||
|
pin += 8;
|
||||||
|
|
||||||
|
sum1 = (denhalf - a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3
|
||||||
|
- a4 * b4 - a5 * b5 - a6 * b6 - a7 * b7) >> denshift;
|
||||||
|
|
||||||
|
del = in[j] - top - sum1;
|
||||||
|
del = (del << chanshift) >> chanshift;
|
||||||
|
pc1[j] = del;
|
||||||
|
del0 = del;
|
||||||
|
|
||||||
|
sg = sign_of_int(del);
|
||||||
|
if ( sg > 0 )
|
||||||
|
{
|
||||||
|
sgn = sign_of_int( b7 );
|
||||||
|
a7 -= sgn;
|
||||||
|
del0 -= 1 * ((sgn * b7) >> denshift);
|
||||||
|
if ( del0 <= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = sign_of_int( b6 );
|
||||||
|
a6 -= sgn;
|
||||||
|
del0 -= 2 * ((sgn * b6) >> denshift);
|
||||||
|
if ( del0 <= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = sign_of_int( b5 );
|
||||||
|
a5 -= sgn;
|
||||||
|
del0 -= 3 * ((sgn * b5) >> denshift);
|
||||||
|
if ( del0 <= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = sign_of_int( b4 );
|
||||||
|
a4 -= sgn;
|
||||||
|
del0 -= 4 * ((sgn * b4) >> denshift);
|
||||||
|
if ( del0 <= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = sign_of_int( b3 );
|
||||||
|
a3 -= sgn;
|
||||||
|
del0 -= 5 * ((sgn * b3) >> denshift);
|
||||||
|
if ( del0 <= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = sign_of_int( b2 );
|
||||||
|
a2 -= sgn;
|
||||||
|
del0 -= 6 * ((sgn * b2) >> denshift);
|
||||||
|
if ( del0 <= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = sign_of_int( b1 );
|
||||||
|
a1 -= sgn;
|
||||||
|
del0 -= 7 * ((sgn * b1) >> denshift);
|
||||||
|
if ( del0 <= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
a0 -= sign_of_int( b0 );
|
||||||
|
}
|
||||||
|
else if ( sg < 0 )
|
||||||
|
{
|
||||||
|
// note: to avoid unnecessary negations, we flip the value of "sgn"
|
||||||
|
sgn = -sign_of_int( b7 );
|
||||||
|
a7 -= sgn;
|
||||||
|
del0 -= 1 * ((sgn * b7) >> denshift);
|
||||||
|
if ( del0 >= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = -sign_of_int( b6 );
|
||||||
|
a6 -= sgn;
|
||||||
|
del0 -= 2 * ((sgn * b6) >> denshift);
|
||||||
|
if ( del0 >= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = -sign_of_int( b5 );
|
||||||
|
a5 -= sgn;
|
||||||
|
del0 -= 3 * ((sgn * b5) >> denshift);
|
||||||
|
if ( del0 >= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = -sign_of_int( b4 );
|
||||||
|
a4 -= sgn;
|
||||||
|
del0 -= 4 * ((sgn * b4) >> denshift);
|
||||||
|
if ( del0 >= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = -sign_of_int( b3 );
|
||||||
|
a3 -= sgn;
|
||||||
|
del0 -= 5 * ((sgn * b3) >> denshift);
|
||||||
|
if ( del0 >= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = -sign_of_int( b2 );
|
||||||
|
a2 -= sgn;
|
||||||
|
del0 -= 6 * ((sgn * b2) >> denshift);
|
||||||
|
if ( del0 >= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sgn = -sign_of_int( b1 );
|
||||||
|
a1 -= sgn;
|
||||||
|
del0 -= 7 * ((sgn * b1) >> denshift);
|
||||||
|
if ( del0 >= 0 )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
a0 += sign_of_int( b0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coefs[0] = a0;
|
||||||
|
coefs[1] = a1;
|
||||||
|
coefs[2] = a2;
|
||||||
|
coefs[3] = a3;
|
||||||
|
coefs[4] = a4;
|
||||||
|
coefs[5] = a5;
|
||||||
|
coefs[6] = a6;
|
||||||
|
coefs[7] = a7;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//pc_block_general:
|
||||||
|
// general case
|
||||||
|
for ( j = lim; j < num; j++ )
|
||||||
|
{
|
||||||
|
LOOP_ALIGN
|
||||||
|
|
||||||
|
top = in[j - lim];
|
||||||
|
pin = in + j - 1;
|
||||||
|
|
||||||
|
sum1 = 0;
|
||||||
|
for ( k = 0; k < numactive; k++ )
|
||||||
|
sum1 -= coefs[k] * (top - pin[-k]);
|
||||||
|
|
||||||
|
del = in[j] - top - ((sum1 + denhalf) >> denshift);
|
||||||
|
del = (del << chanshift) >> chanshift;
|
||||||
|
pc1[j] = del;
|
||||||
|
del0 = del;
|
||||||
|
|
||||||
|
sg = sign_of_int( del );
|
||||||
|
if ( sg > 0 )
|
||||||
|
{
|
||||||
|
for ( k = (numactive - 1); k >= 0; k-- )
|
||||||
|
{
|
||||||
|
dd = top - pin[-k];
|
||||||
|
sgn = sign_of_int( dd );
|
||||||
|
coefs[k] -= sgn;
|
||||||
|
del0 -= (numactive - k) * ((sgn * dd) >> denshift);
|
||||||
|
if ( del0 <= 0 )
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ( sg < 0 )
|
||||||
|
{
|
||||||
|
for ( k = (numactive - 1); k >= 0; k-- )
|
||||||
|
{
|
||||||
|
dd = top - pin[-k];
|
||||||
|
sgn = sign_of_int( dd );
|
||||||
|
coefs[k] += sgn;
|
||||||
|
del0 -= (numactive - k) * ((-sgn * dd) >> denshift);
|
||||||
|
if ( del0 >= 0 )
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
alac/codec/dplib.h
Normal file
61
alac/codec/dplib.h
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
File: dplib.h
|
||||||
|
|
||||||
|
Contains: Dynamic Predictor routines
|
||||||
|
|
||||||
|
Copyright: Copyright (C) 2001-2011 Apple, Inc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __DPLIB_H__
|
||||||
|
#define __DPLIB_H__
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// defines
|
||||||
|
|
||||||
|
#define DENSHIFT_MAX 15
|
||||||
|
#define DENSHIFT_DEFAULT 9
|
||||||
|
#define AINIT 38
|
||||||
|
#define BINIT (-29)
|
||||||
|
#define CINIT (-2)
|
||||||
|
#define NUMCOEPAIRS 16
|
||||||
|
|
||||||
|
// prototypes
|
||||||
|
|
||||||
|
void init_coefs( int16_t * coefs, uint32_t denshift, int32_t numPairs );
|
||||||
|
void copy_coefs( int16_t * srcCoefs, int16_t * dstCoefs, int32_t numPairs );
|
||||||
|
|
||||||
|
// NOTE: these routines read at least "numactive" samples so the i/o buffers must be at least that big
|
||||||
|
|
||||||
|
void pc_block( int32_t * in, int32_t * pc, int32_t num, int16_t * coefs, int32_t numactive, uint32_t chanbits, uint32_t denshift );
|
||||||
|
void unpc_block( int32_t * pc, int32_t * out, int32_t num, int16_t * coefs, int32_t numactive, uint32_t chanbits, uint32_t denshift );
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* __DPLIB_H__ */
|
80
alac/codec/makefile
Normal file
80
alac/codec/makefile
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# libalac make
|
||||||
|
|
||||||
|
CFLAGS = -g -O3 -c
|
||||||
|
LFLAGS = -Wall
|
||||||
|
CC = g++
|
||||||
|
|
||||||
|
SRCDIR = .
|
||||||
|
OBJDIR = ./obj
|
||||||
|
INCLUDES = .
|
||||||
|
|
||||||
|
HEADERS = \
|
||||||
|
$(SRCDIR)/EndianPortable.h \
|
||||||
|
$(SRCDIR)/aglib.h \
|
||||||
|
$(SRCDIR)/ALACAudioTypes.h \
|
||||||
|
$(SRCDIR)/ALACBitUtilities.h\
|
||||||
|
$(SRCDIR)/ALACDecoder.h \
|
||||||
|
$(SRCDIR)/ALACEncoder.h \
|
||||||
|
$(SRCDIR)/dplib.h \
|
||||||
|
$(SRCDIR)/matrixlib.h
|
||||||
|
|
||||||
|
SOURCES = \
|
||||||
|
$(SRCDIR)/EndianPortable.c \
|
||||||
|
$(SRCDIR)/ALACBitUtilities.c \
|
||||||
|
$(SRCDIR)/ALACDecoder.cpp \
|
||||||
|
$(SRCDIR)/ALACEncoder.cpp \
|
||||||
|
$(SRCDIR)/ag_dec.c \
|
||||||
|
$(SRCDIR)/ag_enc.c \
|
||||||
|
$(SRCDIR)/dp_dec.c \
|
||||||
|
$(SRCDIR)/dp_enc.c \
|
||||||
|
$(SRCDIR)/matrix_dec.c \
|
||||||
|
$(SRCDIR)/matrix_enc.c
|
||||||
|
|
||||||
|
OBJS = \
|
||||||
|
EndianPortable.o \
|
||||||
|
ALACBitUtilities.o \
|
||||||
|
ALACDecoder.o \
|
||||||
|
ALACEncoder.o \
|
||||||
|
ag_dec.o \
|
||||||
|
ag_enc.o \
|
||||||
|
dp_dec.o \
|
||||||
|
dp_enc.o \
|
||||||
|
matrix_dec.o \
|
||||||
|
matrix_enc.o
|
||||||
|
|
||||||
|
libalac.a: $(OBJS)
|
||||||
|
ar rcs libalac.a $(OBJS)
|
||||||
|
|
||||||
|
EndianPortable.o : EndianPortable.c
|
||||||
|
$(CC) -I $(INCLUDES) $(CFLAGS) EndianPortable.c
|
||||||
|
|
||||||
|
ALACBitUtilities.o : ALACBitUtilities.c
|
||||||
|
$(CC) -I $(INCLUDES) $(CFLAGS) ALACBitUtilities.c
|
||||||
|
|
||||||
|
ALACDecoder.o : ALACDecoder.cpp
|
||||||
|
$(CC) -I $(INCLUDES) $(CFLAGS) ALACDecoder.cpp
|
||||||
|
|
||||||
|
ALACEncoder.o : ALACEncoder.cpp
|
||||||
|
$(CC) -I $(INCLUDES) $(CFLAGS) ALACEncoder.cpp
|
||||||
|
|
||||||
|
ag_dec.o : ag_dec.c
|
||||||
|
$(CC) -I $(INCLUDES) $(CFLAGS) ag_dec.c
|
||||||
|
|
||||||
|
ag_enc.o : ag_enc.c
|
||||||
|
$(CC) -I $(INCLUDES) $(CFLAGS) ag_enc.c
|
||||||
|
|
||||||
|
dp_dec.o : dp_dec.c
|
||||||
|
$(CC) -I $(INCLUDES) $(CFLAGS) dp_dec.c
|
||||||
|
|
||||||
|
dp_enc.o : dp_enc.c
|
||||||
|
$(CC) -I $(INCLUDES) $(CFLAGS) dp_enc.c
|
||||||
|
|
||||||
|
matrix_dec.o : matrix_dec.c
|
||||||
|
$(CC) -I $(INCLUDES) $(CFLAGS) matrix_dec.c
|
||||||
|
|
||||||
|
matrix_enc.o : matrix_enc.c
|
||||||
|
$(CC) -I $(INCLUDES) $(CFLAGS) matrix_enc.c
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm $(OBJS) libalac.a
|
||||||
|
|
390
alac/codec/matrix_dec.c
Normal file
390
alac/codec/matrix_dec.c
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
File: matrix_dec.c
|
||||||
|
|
||||||
|
Contains: ALAC mixing/matrixing decode routines.
|
||||||
|
|
||||||
|
Copyright: (c) 2004-2011 Apple, Inc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "matrixlib.h"
|
||||||
|
#include "ALACAudioTypes.h"
|
||||||
|
|
||||||
|
// up to 24-bit "offset" macros for the individual bytes of a 20/24-bit word
|
||||||
|
#if TARGET_RT_BIG_ENDIAN
|
||||||
|
#define LBYTE 2
|
||||||
|
#define MBYTE 1
|
||||||
|
#define HBYTE 0
|
||||||
|
#else
|
||||||
|
#define LBYTE 0
|
||||||
|
#define MBYTE 1
|
||||||
|
#define HBYTE 2
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
There is no plain middle-side option; instead there are various mixing
|
||||||
|
modes including middle-side, each lossless, as embodied in the mix()
|
||||||
|
and unmix() functions. These functions exploit a generalized middle-side
|
||||||
|
transformation:
|
||||||
|
|
||||||
|
u := [(rL + (m-r)R)/m];
|
||||||
|
v := L - R;
|
||||||
|
|
||||||
|
where [ ] denotes integer floor. The (lossless) inverse is
|
||||||
|
|
||||||
|
L = u + v - [rV/m];
|
||||||
|
R = L - v;
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 16-bit routines
|
||||||
|
|
||||||
|
void unmix16( int32_t * u, int32_t * v, int16_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres )
|
||||||
|
{
|
||||||
|
int16_t * op = out;
|
||||||
|
int32_t j;
|
||||||
|
|
||||||
|
if ( mixres != 0 )
|
||||||
|
{
|
||||||
|
/* matrixed stereo */
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
int32_t l, r;
|
||||||
|
|
||||||
|
l = u[j] + v[j] - ((mixres * v[j]) >> mixbits);
|
||||||
|
r = l - v[j];
|
||||||
|
|
||||||
|
op[0] = (int16_t) l;
|
||||||
|
op[1] = (int16_t) r;
|
||||||
|
op += stride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Conventional separated stereo. */
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
op[0] = (int16_t) u[j];
|
||||||
|
op[1] = (int16_t) v[j];
|
||||||
|
op += stride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20-bit routines
|
||||||
|
// - the 20 bits of data are left-justified in 3 bytes of storage but right-aligned for input/output predictor buffers
|
||||||
|
|
||||||
|
void unmix20( int32_t * u, int32_t * v, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres )
|
||||||
|
{
|
||||||
|
uint8_t * op = out;
|
||||||
|
int32_t j;
|
||||||
|
|
||||||
|
if ( mixres != 0 )
|
||||||
|
{
|
||||||
|
/* matrixed stereo */
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
int32_t l, r;
|
||||||
|
|
||||||
|
l = u[j] + v[j] - ((mixres * v[j]) >> mixbits);
|
||||||
|
r = l - v[j];
|
||||||
|
|
||||||
|
l <<= 4;
|
||||||
|
r <<= 4;
|
||||||
|
|
||||||
|
op[HBYTE] = (uint8_t)((l >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((l >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((l >> 0) & 0xffu);
|
||||||
|
op += 3;
|
||||||
|
|
||||||
|
op[HBYTE] = (uint8_t)((r >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((r >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((r >> 0) & 0xffu);
|
||||||
|
|
||||||
|
op += (stride - 1) * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Conventional separated stereo. */
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
int32_t val;
|
||||||
|
|
||||||
|
val = u[j] << 4;
|
||||||
|
op[HBYTE] = (uint8_t)((val >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((val >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((val >> 0) & 0xffu);
|
||||||
|
op += 3;
|
||||||
|
|
||||||
|
val = v[j] << 4;
|
||||||
|
op[HBYTE] = (uint8_t)((val >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((val >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((val >> 0) & 0xffu);
|
||||||
|
|
||||||
|
op += (stride - 1) * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 24-bit routines
|
||||||
|
// - the 24 bits of data are right-justified in the input/output predictor buffers
|
||||||
|
|
||||||
|
void unmix24( int32_t * u, int32_t * v, uint8_t * out, uint32_t stride, int32_t numSamples,
|
||||||
|
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted )
|
||||||
|
{
|
||||||
|
uint8_t * op = out;
|
||||||
|
int32_t shift = bytesShifted * 8;
|
||||||
|
int32_t l, r;
|
||||||
|
int32_t j, k;
|
||||||
|
|
||||||
|
if ( mixres != 0 )
|
||||||
|
{
|
||||||
|
/* matrixed stereo */
|
||||||
|
if ( bytesShifted != 0 )
|
||||||
|
{
|
||||||
|
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
|
||||||
|
{
|
||||||
|
l = u[j] + v[j] - ((mixres * v[j]) >> mixbits);
|
||||||
|
r = l - v[j];
|
||||||
|
|
||||||
|
l = (l << shift) | (uint32_t) shiftUV[k + 0];
|
||||||
|
r = (r << shift) | (uint32_t) shiftUV[k + 1];
|
||||||
|
|
||||||
|
op[HBYTE] = (uint8_t)((l >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((l >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((l >> 0) & 0xffu);
|
||||||
|
op += 3;
|
||||||
|
|
||||||
|
op[HBYTE] = (uint8_t)((r >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((r >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((r >> 0) & 0xffu);
|
||||||
|
|
||||||
|
op += (stride - 1) * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
l = u[j] + v[j] - ((mixres * v[j]) >> mixbits);
|
||||||
|
r = l - v[j];
|
||||||
|
|
||||||
|
op[HBYTE] = (uint8_t)((l >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((l >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((l >> 0) & 0xffu);
|
||||||
|
op += 3;
|
||||||
|
|
||||||
|
op[HBYTE] = (uint8_t)((r >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((r >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((r >> 0) & 0xffu);
|
||||||
|
|
||||||
|
op += (stride - 1) * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Conventional separated stereo. */
|
||||||
|
if ( bytesShifted != 0 )
|
||||||
|
{
|
||||||
|
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
|
||||||
|
{
|
||||||
|
l = u[j];
|
||||||
|
r = v[j];
|
||||||
|
|
||||||
|
l = (l << shift) | (uint32_t) shiftUV[k + 0];
|
||||||
|
r = (r << shift) | (uint32_t) shiftUV[k + 1];
|
||||||
|
|
||||||
|
op[HBYTE] = (uint8_t)((l >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((l >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((l >> 0) & 0xffu);
|
||||||
|
op += 3;
|
||||||
|
|
||||||
|
op[HBYTE] = (uint8_t)((r >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((r >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((r >> 0) & 0xffu);
|
||||||
|
|
||||||
|
op += (stride - 1) * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
int32_t val;
|
||||||
|
|
||||||
|
val = u[j];
|
||||||
|
op[HBYTE] = (uint8_t)((val >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((val >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((val >> 0) & 0xffu);
|
||||||
|
op += 3;
|
||||||
|
|
||||||
|
val = v[j];
|
||||||
|
op[HBYTE] = (uint8_t)((val >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((val >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((val >> 0) & 0xffu);
|
||||||
|
|
||||||
|
op += (stride - 1) * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 32-bit routines
|
||||||
|
// - note that these really expect the internal data width to be < 32 but the arrays are 32-bit
|
||||||
|
// - otherwise, the calculations might overflow into the 33rd bit and be lost
|
||||||
|
// - therefore, these routines deal with the specified "unused lower" bytes in the "shift" buffers
|
||||||
|
|
||||||
|
void unmix32( int32_t * u, int32_t * v, int32_t * out, uint32_t stride, int32_t numSamples,
|
||||||
|
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted )
|
||||||
|
{
|
||||||
|
int32_t * op = out;
|
||||||
|
int32_t shift = bytesShifted * 8;
|
||||||
|
int32_t l, r;
|
||||||
|
int32_t j, k;
|
||||||
|
|
||||||
|
if ( mixres != 0 )
|
||||||
|
{
|
||||||
|
//Assert( bytesShifted != 0 );
|
||||||
|
|
||||||
|
/* matrixed stereo with shift */
|
||||||
|
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
|
||||||
|
{
|
||||||
|
int32_t lt, rt;
|
||||||
|
|
||||||
|
lt = u[j];
|
||||||
|
rt = v[j];
|
||||||
|
|
||||||
|
l = lt + rt - ((mixres * rt) >> mixbits);
|
||||||
|
r = l - rt;
|
||||||
|
|
||||||
|
op[0] = (l << shift) | (uint32_t) shiftUV[k + 0];
|
||||||
|
op[1] = (r << shift) | (uint32_t) shiftUV[k + 1];
|
||||||
|
op += stride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ( bytesShifted == 0 )
|
||||||
|
{
|
||||||
|
/* interleaving w/o shift */
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
op[0] = u[j];
|
||||||
|
op[1] = v[j];
|
||||||
|
op += stride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* interleaving with shift */
|
||||||
|
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
|
||||||
|
{
|
||||||
|
op[0] = (u[j] << shift) | (uint32_t) shiftUV[k + 0];
|
||||||
|
op[1] = (v[j] << shift) | (uint32_t) shiftUV[k + 1];
|
||||||
|
op += stride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20/24-bit <-> 32-bit helper routines (not really matrixing but convenient to put here)
|
||||||
|
|
||||||
|
void copyPredictorTo24( int32_t * in, uint8_t * out, uint32_t stride, int32_t numSamples )
|
||||||
|
{
|
||||||
|
uint8_t * op = out;
|
||||||
|
int32_t j;
|
||||||
|
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
int32_t val = in[j];
|
||||||
|
|
||||||
|
op[HBYTE] = (uint8_t)((val >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((val >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((val >> 0) & 0xffu);
|
||||||
|
op += (stride * 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void copyPredictorTo24Shift( int32_t * in, uint16_t * shift, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t bytesShifted )
|
||||||
|
{
|
||||||
|
uint8_t * op = out;
|
||||||
|
int32_t shiftVal = bytesShifted * 8;
|
||||||
|
int32_t j;
|
||||||
|
|
||||||
|
//Assert( bytesShifted != 0 );
|
||||||
|
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
int32_t val = in[j];
|
||||||
|
|
||||||
|
val = (val << shiftVal) | (uint32_t) shift[j];
|
||||||
|
|
||||||
|
op[HBYTE] = (uint8_t)((val >> 16) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((val >> 8) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((val >> 0) & 0xffu);
|
||||||
|
op += (stride * 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void copyPredictorTo20( int32_t * in, uint8_t * out, uint32_t stride, int32_t numSamples )
|
||||||
|
{
|
||||||
|
uint8_t * op = out;
|
||||||
|
int32_t j;
|
||||||
|
|
||||||
|
// 32-bit predictor values are right-aligned but 20-bit output values should be left-aligned
|
||||||
|
// in the 24-bit output buffer
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
int32_t val = in[j];
|
||||||
|
|
||||||
|
op[HBYTE] = (uint8_t)((val >> 12) & 0xffu);
|
||||||
|
op[MBYTE] = (uint8_t)((val >> 4) & 0xffu);
|
||||||
|
op[LBYTE] = (uint8_t)((val << 4) & 0xffu);
|
||||||
|
op += (stride * 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void copyPredictorTo32( int32_t * in, int32_t * out, uint32_t stride, int32_t numSamples )
|
||||||
|
{
|
||||||
|
int32_t i, j;
|
||||||
|
|
||||||
|
// this is only a subroutine to abstract the "iPod can only output 16-bit data" problem
|
||||||
|
for ( i = 0, j = 0; i < numSamples; i++, j += stride )
|
||||||
|
out[j] = in[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
void copyPredictorTo32Shift( int32_t * in, uint16_t * shift, int32_t * out, uint32_t stride, int32_t numSamples, int32_t bytesShifted )
|
||||||
|
{
|
||||||
|
int32_t * op = out;
|
||||||
|
uint32_t shiftVal = bytesShifted * 8;
|
||||||
|
int32_t j;
|
||||||
|
|
||||||
|
//Assert( bytesShifted != 0 );
|
||||||
|
|
||||||
|
// this is only a subroutine to abstract the "iPod can only output 16-bit data" problem
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
op[0] = (in[j] << shiftVal) | (uint32_t) shift[j];
|
||||||
|
op += stride;
|
||||||
|
}
|
||||||
|
}
|
342
alac/codec/matrix_enc.c
Normal file
342
alac/codec/matrix_enc.c
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
File: matrix_enc.c
|
||||||
|
|
||||||
|
Contains: ALAC mixing/matrixing encode routines.
|
||||||
|
|
||||||
|
Copyright: (c) 2004-2011 Apple, Inc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "matrixlib.h"
|
||||||
|
#include "ALACAudioTypes.h"
|
||||||
|
|
||||||
|
// up to 24-bit "offset" macros for the individual bytes of a 20/24-bit word
|
||||||
|
#if TARGET_RT_BIG_ENDIAN
|
||||||
|
#define LBYTE 2
|
||||||
|
#define MBYTE 1
|
||||||
|
#define HBYTE 0
|
||||||
|
#else
|
||||||
|
#define LBYTE 0
|
||||||
|
#define MBYTE 1
|
||||||
|
#define HBYTE 2
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
There is no plain middle-side option; instead there are various mixing
|
||||||
|
modes including middle-side, each lossless, as embodied in the mix()
|
||||||
|
and unmix() functions. These functions exploit a generalized middle-side
|
||||||
|
transformation:
|
||||||
|
|
||||||
|
u := [(rL + (m-r)R)/m];
|
||||||
|
v := L - R;
|
||||||
|
|
||||||
|
where [ ] denotes integer floor. The (lossless) inverse is
|
||||||
|
|
||||||
|
L = u + v - [rV/m];
|
||||||
|
R = L - v;
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 16-bit routines
|
||||||
|
|
||||||
|
void mix16( int16_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples, int32_t mixbits, int32_t mixres )
|
||||||
|
{
|
||||||
|
int16_t * ip = in;
|
||||||
|
int32_t j;
|
||||||
|
|
||||||
|
if ( mixres != 0 )
|
||||||
|
{
|
||||||
|
int32_t mod = 1 << mixbits;
|
||||||
|
int32_t m2;
|
||||||
|
|
||||||
|
/* matrixed stereo */
|
||||||
|
m2 = mod - mixres;
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
int32_t l, r;
|
||||||
|
|
||||||
|
l = (int32_t) ip[0];
|
||||||
|
r = (int32_t) ip[1];
|
||||||
|
ip += stride;
|
||||||
|
u[j] = (mixres * l + m2 * r) >> mixbits;
|
||||||
|
v[j] = l - r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Conventional separated stereo. */
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
u[j] = (int32_t) ip[0];
|
||||||
|
v[j] = (int32_t) ip[1];
|
||||||
|
ip += stride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20-bit routines
|
||||||
|
// - the 20 bits of data are left-justified in 3 bytes of storage but right-aligned for input/output predictor buffers
|
||||||
|
|
||||||
|
void mix20( uint8_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples, int32_t mixbits, int32_t mixres )
|
||||||
|
{
|
||||||
|
int32_t l, r;
|
||||||
|
uint8_t * ip = in;
|
||||||
|
int32_t j;
|
||||||
|
|
||||||
|
if ( mixres != 0 )
|
||||||
|
{
|
||||||
|
/* matrixed stereo */
|
||||||
|
int32_t mod = 1 << mixbits;
|
||||||
|
int32_t m2 = mod - mixres;
|
||||||
|
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
l = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
l = (l << 8) >> 12;
|
||||||
|
ip += 3;
|
||||||
|
|
||||||
|
r = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
r = (r << 8) >> 12;
|
||||||
|
ip += (stride - 1) * 3;
|
||||||
|
|
||||||
|
u[j] = (mixres * l + m2 * r) >> mixbits;
|
||||||
|
v[j] = l - r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Conventional separated stereo. */
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
l = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
u[j] = (l << 8) >> 12;
|
||||||
|
ip += 3;
|
||||||
|
|
||||||
|
r = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
v[j] = (r << 8) >> 12;
|
||||||
|
ip += (stride - 1) * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 24-bit routines
|
||||||
|
// - the 24 bits of data are right-justified in the input/output predictor buffers
|
||||||
|
|
||||||
|
void mix24( uint8_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples,
|
||||||
|
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted )
|
||||||
|
{
|
||||||
|
int32_t l, r;
|
||||||
|
uint8_t * ip = in;
|
||||||
|
int32_t shift = bytesShifted * 8;
|
||||||
|
uint32_t mask = (1ul << shift) - 1;
|
||||||
|
int32_t j, k;
|
||||||
|
|
||||||
|
if ( mixres != 0 )
|
||||||
|
{
|
||||||
|
/* matrixed stereo */
|
||||||
|
int32_t mod = 1 << mixbits;
|
||||||
|
int32_t m2 = mod - mixres;
|
||||||
|
|
||||||
|
if ( bytesShifted != 0 )
|
||||||
|
{
|
||||||
|
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
|
||||||
|
{
|
||||||
|
l = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
l = (l << 8) >> 8;
|
||||||
|
ip += 3;
|
||||||
|
|
||||||
|
r = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
r = (r << 8) >> 8;
|
||||||
|
ip += (stride - 1) * 3;
|
||||||
|
|
||||||
|
shiftUV[k + 0] = (uint16_t)(l & mask);
|
||||||
|
shiftUV[k + 1] = (uint16_t)(r & mask);
|
||||||
|
|
||||||
|
l >>= shift;
|
||||||
|
r >>= shift;
|
||||||
|
|
||||||
|
u[j] = (mixres * l + m2 * r) >> mixbits;
|
||||||
|
v[j] = l - r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
l = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
l = (l << 8) >> 8;
|
||||||
|
ip += 3;
|
||||||
|
|
||||||
|
r = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
r = (r << 8) >> 8;
|
||||||
|
ip += (stride - 1) * 3;
|
||||||
|
|
||||||
|
u[j] = (mixres * l + m2 * r) >> mixbits;
|
||||||
|
v[j] = l - r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Conventional separated stereo. */
|
||||||
|
if ( bytesShifted != 0 )
|
||||||
|
{
|
||||||
|
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
|
||||||
|
{
|
||||||
|
l = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
l = (l << 8) >> 8;
|
||||||
|
ip += 3;
|
||||||
|
|
||||||
|
r = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
r = (r << 8) >> 8;
|
||||||
|
ip += (stride - 1) * 3;
|
||||||
|
|
||||||
|
shiftUV[k + 0] = (uint16_t)(l & mask);
|
||||||
|
shiftUV[k + 1] = (uint16_t)(r & mask);
|
||||||
|
|
||||||
|
l >>= shift;
|
||||||
|
r >>= shift;
|
||||||
|
|
||||||
|
u[j] = l;
|
||||||
|
v[j] = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
l = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
u[j] = (l << 8) >> 8;
|
||||||
|
ip += 3;
|
||||||
|
|
||||||
|
r = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
v[j] = (r << 8) >> 8;
|
||||||
|
ip += (stride - 1) * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 32-bit routines
|
||||||
|
// - note that these really expect the internal data width to be < 32 but the arrays are 32-bit
|
||||||
|
// - otherwise, the calculations might overflow into the 33rd bit and be lost
|
||||||
|
// - therefore, these routines deal with the specified "unused lower" bytes in the "shift" buffers
|
||||||
|
|
||||||
|
void mix32( int32_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples,
|
||||||
|
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted )
|
||||||
|
{
|
||||||
|
int32_t * ip = in;
|
||||||
|
int32_t shift = bytesShifted * 8;
|
||||||
|
uint32_t mask = (1ul << shift) - 1;
|
||||||
|
int32_t l, r;
|
||||||
|
int32_t j, k;
|
||||||
|
|
||||||
|
if ( mixres != 0 )
|
||||||
|
{
|
||||||
|
int32_t mod = 1 << mixbits;
|
||||||
|
int32_t m2;
|
||||||
|
|
||||||
|
//Assert( bytesShifted != 0 );
|
||||||
|
|
||||||
|
/* matrixed stereo with shift */
|
||||||
|
m2 = mod - mixres;
|
||||||
|
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
|
||||||
|
{
|
||||||
|
l = ip[0];
|
||||||
|
r = ip[1];
|
||||||
|
ip += stride;
|
||||||
|
|
||||||
|
shiftUV[k + 0] = (uint16_t)(l & mask);
|
||||||
|
shiftUV[k + 1] = (uint16_t)(r & mask);
|
||||||
|
|
||||||
|
l >>= shift;
|
||||||
|
r >>= shift;
|
||||||
|
|
||||||
|
u[j] = (mixres * l + m2 * r) >> mixbits;
|
||||||
|
v[j] = l - r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ( bytesShifted == 0 )
|
||||||
|
{
|
||||||
|
/* de-interleaving w/o shift */
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
u[j] = ip[0];
|
||||||
|
v[j] = ip[1];
|
||||||
|
ip += stride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* de-interleaving with shift */
|
||||||
|
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
|
||||||
|
{
|
||||||
|
l = ip[0];
|
||||||
|
r = ip[1];
|
||||||
|
ip += stride;
|
||||||
|
|
||||||
|
shiftUV[k + 0] = (uint16_t)(l & mask);
|
||||||
|
shiftUV[k + 1] = (uint16_t)(r & mask);
|
||||||
|
|
||||||
|
l >>= shift;
|
||||||
|
r >>= shift;
|
||||||
|
|
||||||
|
u[j] = l;
|
||||||
|
v[j] = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20/24-bit <-> 32-bit helper routines (not really matrixing but convenient to put here)
|
||||||
|
|
||||||
|
void copy20ToPredictor( uint8_t * in, uint32_t stride, int32_t * out, int32_t numSamples )
|
||||||
|
{
|
||||||
|
uint8_t * ip = in;
|
||||||
|
int32_t j;
|
||||||
|
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
int32_t val;
|
||||||
|
|
||||||
|
// 20-bit values are left-aligned in the 24-bit input buffer but right-aligned in the 32-bit output buffer
|
||||||
|
val = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
out[j] = (val << 8) >> 12;
|
||||||
|
ip += stride * 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void copy24ToPredictor( uint8_t * in, uint32_t stride, int32_t * out, int32_t numSamples )
|
||||||
|
{
|
||||||
|
uint8_t * ip = in;
|
||||||
|
int32_t j;
|
||||||
|
|
||||||
|
for ( j = 0; j < numSamples; j++ )
|
||||||
|
{
|
||||||
|
int32_t val;
|
||||||
|
|
||||||
|
val = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
|
||||||
|
out[j] = (val << 8) >> 8;
|
||||||
|
ip += stride * 3;
|
||||||
|
}
|
||||||
|
}
|
80
alac/codec/matrixlib.h
Normal file
80
alac/codec/matrixlib.h
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2011 Apple Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_START@
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @APPLE_APACHE_LICENSE_HEADER_END@
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
File: matrixlib.h
|
||||||
|
|
||||||
|
Contains: ALAC mixing/matrixing routines to/from 32-bit predictor buffers.
|
||||||
|
|
||||||
|
Copyright: Copyright (C) 2004 to 2011 Apple, Inc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __MATRIXLIB_H
|
||||||
|
#define __MATRIXLIB_H
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 16-bit routines
|
||||||
|
void mix16( int16_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples, int32_t mixbits, int32_t mixres );
|
||||||
|
void unmix16( int32_t * u, int32_t * v, int16_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres );
|
||||||
|
|
||||||
|
// 20-bit routines
|
||||||
|
void mix20( uint8_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples, int32_t mixbits, int32_t mixres );
|
||||||
|
void unmix20( int32_t * u, int32_t * v, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres );
|
||||||
|
|
||||||
|
// 24-bit routines
|
||||||
|
// - 24-bit data sometimes compresses better by shifting off the bottom byte so these routines deal with
|
||||||
|
// the specified "unused lower bytes" in the combined "shift" buffer
|
||||||
|
void mix24( uint8_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples,
|
||||||
|
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted );
|
||||||
|
void unmix24( int32_t * u, int32_t * v, uint8_t * out, uint32_t stride, int32_t numSamples,
|
||||||
|
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted );
|
||||||
|
|
||||||
|
// 32-bit routines
|
||||||
|
// - note that these really expect the internal data width to be < 32-bit but the arrays are 32-bit
|
||||||
|
// - otherwise, the calculations might overflow into the 33rd bit and be lost
|
||||||
|
// - therefore, these routines deal with the specified "unused lower" bytes in the combined "shift" buffer
|
||||||
|
void mix32( int32_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples,
|
||||||
|
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted );
|
||||||
|
void unmix32( int32_t * u, int32_t * v, int32_t * out, uint32_t stride, int32_t numSamples,
|
||||||
|
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted );
|
||||||
|
|
||||||
|
// 20/24/32-bit <-> 32-bit helper routines (not really matrixing but convenient to put here)
|
||||||
|
void copy20ToPredictor( uint8_t * in, uint32_t stride, int32_t * out, int32_t numSamples );
|
||||||
|
void copy24ToPredictor( uint8_t * in, uint32_t stride, int32_t * out, int32_t numSamples );
|
||||||
|
|
||||||
|
void copyPredictorTo24( int32_t * in, uint8_t * out, uint32_t stride, int32_t numSamples );
|
||||||
|
void copyPredictorTo24Shift( int32_t * in, uint16_t * shift, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t bytesShifted );
|
||||||
|
void copyPredictorTo20( int32_t * in, uint8_t * out, uint32_t stride, int32_t numSamples );
|
||||||
|
|
||||||
|
void copyPredictorTo32( int32_t * in, int32_t * out, uint32_t stride, int32_t numSamples );
|
||||||
|
void copyPredictorTo32Shift( int32_t * in, uint16_t * shift, int32_t * out, uint32_t stride, int32_t numSamples, int32_t bytesShifted );
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* __MATRIXLIB_H */
|
44
alac/readme.md
Normal file
44
alac/readme.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
The Apple Lossless Format
|
||||||
|
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
Apple Lossless supports the following features. Not all of these are implemented in alacconvert, though they are in the codec code provided.
|
||||||
|
|
||||||
|
1. Bit depths 16, 20, 24 and 32 bits.
|
||||||
|
2. Any arbitrary integer sample rate from 1 to 384,000 Hz. In theory rates up to 4,294,967,295 (2^32 - 1) Hz could be supported.
|
||||||
|
3. From one to eight channels are supported. Channel orders for the supported formats are described as:
|
||||||
|
Num Chan Order
|
||||||
|
1 mono
|
||||||
|
2 stereo (Left, Right)
|
||||||
|
3 MPEG 3.0 B (Center, Left, Right)
|
||||||
|
4 MPEG 4.0 B (Center, Left, Right, Center Surround)
|
||||||
|
5 MPEG 5.0 D (Center, Left, Right, Left Surround, Right Surround)
|
||||||
|
6 MPEG 5.1 D (Center, Left, Right, Left Surround, Right Surround, Low Frequency Effects)
|
||||||
|
7 Apple AAC 6.1 (Center, Left, Right, Left Surround, Right Surround, Center Surround, Low Frequency Effects)
|
||||||
|
8 MPEG 7.1 B (Center, Left Center, Right Center, Left, Right, Left Surround, Right Surround, Low Frequency Effects)
|
||||||
|
4. Packet size defaults to 4096 sample frames of audio per packet. Other packet sizes are certainly possible. However, non-default packet sizes are not guaranteed to work properly on all hardware devices that support Apple Lossless. Packets above 16,384 sample frames are not supported.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
This package contains the sources for the Apple Lossless (ALAC) encoder and decoder.
|
||||||
|
|
||||||
|
The "codec" directory contains all the sources necessary for a functioning codec. Also includes is a makefile that will build libalac.a on a UNIX/Linux machine.
|
||||||
|
|
||||||
|
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
ALACconvert
|
||||||
|
|
||||||
|
The convert-utility directory contains sources to build alacconvert which is a simple utility that demonstrates how to use the included ALAC encoder and decoder.
|
||||||
|
|
||||||
|
alacconvert supports the following formats:
|
||||||
|
|
||||||
|
1. 16- or 24-bit mono or stereo .wav files where the data is little endian integer. Extended WAVE format chunks are not handled.
|
||||||
|
2. 16- or 24-bit mono or stereo .caf (Core Audio Format) files as well as certain multi-channel configurations where the data is big or little endian integer. It does no channel order manipulation.
|
||||||
|
3. ALAC .caf files.
|
||||||
|
|
||||||
|
Three project are provided to build a command line utility called alacconvert that converts cpm data to ALAC or vice versa. A Mac OS X Xcode project, A Windows Visual Studio project, and a generic UNIX/Linux make file.
|
||||||
|
|
||||||
|
Note: When building on Windows, if you are using a version of Visual Studio before Visual Studio 2010, <stdint.h> is not installed. You will need to acquire this file on your own. It can be put in the same directory as the project.
|
||||||
|
|
||||||
|
|
||||||
|
|
25
ascii_txt.txt
Normal file
25
ascii_txt.txt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
|
||||||
|
_____ _______ _____ _____
|
||||||
|
/\ \ /::\ \ /\ \ /\ \
|
||||||
|
/::\ \ /::::\ \ /::\____\ /::\ \
|
||||||
|
/::::\ \ /::::::\ \ /:::/ / /::::\ \
|
||||||
|
/::::::\ \ /::::::::\ \ /:::/ / /::::::\ \
|
||||||
|
/:::/\:::\ \ /:::/~~\:::\ \ /:::/ / /:::/\:::\ \
|
||||||
|
/:::/__\:::\ \ /:::/ \:::\ \ /:::/____/ /:::/__\:::\ \
|
||||||
|
/::::\ \:::\ \ /:::/ / \:::\ \ /::::\ \ /::::\ \:::\ \
|
||||||
|
/::::::\ \:::\ \ /:::/____/ \:::\____\ /::::::\____\________ /::::::\ \:::\ \
|
||||||
|
/:::/\:::\ \:::\____\ |:::| | |:::| | /:::/\:::::::::::\ \ /:::/\:::\ \:::\ \
|
||||||
|
/:::/ \:::\ \:::| ||:::|____| |:::| |/:::/ |:::::::::::\____\/:::/__\:::\ \:::\____\
|
||||||
|
\::/ \:::\ /:::|____| \:::\ \ /:::/ / \::/ |::|~~~|~~~~~ \:::\ \:::\ \::/ /
|
||||||
|
\/_____/\:::\/:::/ / \:::\ \ /:::/ / \/____|::| | \:::\ \:::\ \/____/
|
||||||
|
\::::::/ / \:::\ /:::/ / |::| | \:::\ \:::\ \
|
||||||
|
\::::/ / \:::\__/:::/ / |::| | \:::\ \:::\____\
|
||||||
|
\::/____/ \::::::::/ / |::| | \:::\ \::/ /
|
||||||
|
~~ \::::::/ / |::| | \:::\ \/____/
|
||||||
|
\::::/ / |::| | \:::\ \
|
||||||
|
\::/____/ \::| | \:::\____\
|
||||||
|
~~ \:| | \::/ /
|
||||||
|
\|___| \/____/
|
||||||
|
|
||||||
|
|
1
backend-services/README.md
Normal file
1
backend-services/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
haii!! these files are made for the poke's server - if u wanna use them on ur server u might have to change the directories :p
|
40
backend-services/scripts/inv-refresh-token.sh
Normal file
40
backend-services/scripts/inv-refresh-token.sh
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024-20xx Poke! (https://codeberg.org/ashley/poke)
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
scriptDir=$(dirname "$(readlink -f "$0")")
|
||||||
|
|
||||||
|
output=$(docker run quay.io/invidious/youtube-trusted-session-generator)
|
||||||
|
|
||||||
|
visitor_data=$(echo "$output" | grep -oP '(?<=visitor_data: )[^ ]+')
|
||||||
|
po_token=$(echo "$output" | grep -oP '(?<=po_token: )[^ ]+')
|
||||||
|
|
||||||
|
if [ -z "$visitor_data" ] || [ -z "$po_token" ]; then
|
||||||
|
echo "Error: Could not generate visitor_data or po_token."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -i "s/visitor_data: .*/visitor_data: $visitor_data/g" $scriptDir/../services/invidious/docker-compose.yml
|
||||||
|
sed -i "s/po_token: .*/po_token: $po_token/g" $scriptDir/../services/invidious/docker-compose.yml
|
||||||
|
|
||||||
|
cd $scriptDir/../services/invidious
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
echo "Successfully updated visitor_data and po_token on Invidious."
|
||||||
|
|
164
backend-services/scripts/inv-restart-docker.sh
Normal file
164
backend-services/scripts/inv-restart-docker.sh
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024-20xx Poke! (https://codeberg.org/ashley/poke)
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
# Function to generate a random Chrome version
|
||||||
|
generate_random_chrome_version() {
|
||||||
|
major=$((RANDOM % 100 + 1)) # Major version 1-99
|
||||||
|
minor=$((RANDOM % 100)) # Minor version 0-99
|
||||||
|
build=$((RANDOM % 10000)) # Build version 0-9999
|
||||||
|
patch=$((RANDOM % 100)) # Patch version 0-99
|
||||||
|
echo "$major.$minor.$build.$patch"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_services() {
|
||||||
|
echo "Restarting services..."
|
||||||
|
|
||||||
|
# Navigate to the script directory
|
||||||
|
scriptDir=$(dirname "$(readlink -f "$0")")
|
||||||
|
|
||||||
|
cd "$scriptDir/../services/invidious" || { echo "Error: Failed to navigate to $scriptDir/../services/invidious"; exit 1; }
|
||||||
|
|
||||||
|
docker compose down
|
||||||
|
echo "Services stopped. Restarting..."
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
echo "Services restarted successfully."
|
||||||
|
|
||||||
|
/home/qt/globe/scripts/inv-update-token.sh
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_playlist() {
|
||||||
|
local playlist_id="$1"
|
||||||
|
response=$(curl -s -w "%{http_code}" -o /tmp/playlist_data.json "https://invid-api.poketube.fun/api/v1/playlists/${playlist_id}")
|
||||||
|
|
||||||
|
if [ "$response" -eq 502 ] || [ "$response" -eq 500 ] || [ "$response" -eq 403 ]; then
|
||||||
|
echo "Error: Failed to fetch playlist data. HTTP Status: $response"
|
||||||
|
restart_services
|
||||||
|
return 1
|
||||||
|
elif [ "$response" -ne 200 ]; then
|
||||||
|
echo "Error: Failed to fetch playlist data. HTTP Status: $response"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_video_ids() {
|
||||||
|
local json_data="$1"
|
||||||
|
video_ids=$(jq -r '.videos[].videoId' "$json_data")
|
||||||
|
if [ -z "$video_ids" ]; then
|
||||||
|
echo "Error: Failed to extract video IDs from the playlist data."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "$video_ids"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Playlist IDs to fetch
|
||||||
|
playlist_ids=("PLMws9SCqJ1JCeVMVPsdamuUM0HK0MbA6g")
|
||||||
|
|
||||||
|
# Base URL for the API
|
||||||
|
base_url="http://localhost:54301/latest_version?id="
|
||||||
|
|
||||||
|
# Pick a random playlist (without using invalid options in shuf)
|
||||||
|
random_playlist_id="PLMC9KNkIncKvYin_USF1qoJQnIyMAfRxl"
|
||||||
|
echo "Randomly selected playlist: $random_playlist_id"
|
||||||
|
|
||||||
|
# Fetch playlist JSON data
|
||||||
|
fetch_playlist "$random_playlist_id"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Playlist fetch failed. Restarting services..."
|
||||||
|
restart_services # Restart services before exiting
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract video IDs from the playlist
|
||||||
|
video_ids=($(extract_video_ids "/tmp/playlist_data.json"))
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to extract video IDs. Exiting..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Shuffle video IDs and pick 4 random videos
|
||||||
|
shuffled_video_ids=($(shuf -e "${video_ids[@]}" | head -n 4))
|
||||||
|
|
||||||
|
error_count=0
|
||||||
|
all_errors=(500 502 403)
|
||||||
|
|
||||||
|
for video_id in "${shuffled_video_ids[@]}"; do
|
||||||
|
# Add a cache buster query (unique random number)
|
||||||
|
unique_param=$RANDOM
|
||||||
|
url="${base_url}${video_id}&itag=18&local=true&_=${unique_param}"
|
||||||
|
|
||||||
|
# Generate a random Chrome version
|
||||||
|
chrome_version=$(generate_random_chrome_version)
|
||||||
|
|
||||||
|
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$chrome_version Safari/537.36"
|
||||||
|
|
||||||
|
response_headers=$(curl -s -D - -H "Cache-Control: no-cache, no-store, must-revalidate" \
|
||||||
|
-H "Pragma: no-cache" -H "Expires: 0" -A "$user_agent" "$url" -o /dev/null)
|
||||||
|
|
||||||
|
# Extract ETag and last modified info (if available)
|
||||||
|
etag=$(echo "$response_headers" | grep -i ETag | awk '{print $2}' | tr -d '"')
|
||||||
|
last_modified=$(echo "$response_headers" | grep -i Last-Modified | cut -d' ' -f2-)
|
||||||
|
|
||||||
|
# Use conditional request if ETag is present
|
||||||
|
if [ -n "$etag" ]; then
|
||||||
|
status_code=$(curl -s -o /dev/null -w "%{http_code}" -H "If-None-Match: $etag" \
|
||||||
|
-H "Cache-Control: no-cache, no-store, must-revalidate" -A "$user_agent" "$url")
|
||||||
|
else
|
||||||
|
status_code=$(curl -s -o /dev/null -w "%{http_code}" -A "$user_agent" "$url")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Echo the status code for visibility
|
||||||
|
echo "Checking URL: $url"
|
||||||
|
echo "User Agent: $user_agent"
|
||||||
|
echo "HTTP Status Code for ID $video_id: $status_code"
|
||||||
|
|
||||||
|
if [[ " ${all_errors[@]} " =~ " ${status_code} " ]]; then
|
||||||
|
echo "Error: Received $status_code for ID $video_id."
|
||||||
|
error_count=$((error_count + 1))
|
||||||
|
|
||||||
|
# Run the token refresh script
|
||||||
|
echo "Running inv-update-token.sh for ID $video_id..."
|
||||||
|
/home/qt/globe/scripts/inv-update-token.sh
|
||||||
|
/home/qt/globe/scripts/inv-update-token.sh
|
||||||
|
echo "inv-update-token.sh script executed successfully."
|
||||||
|
|
||||||
|
# Fetch the video again after token refresh
|
||||||
|
status_code=$(curl -s -o /dev/null -w "%{http_code}" -A "$user_agent" "$url")
|
||||||
|
echo "Post-token-refresh Status Code for ID $video_id: $status_code"
|
||||||
|
|
||||||
|
# Check if it still results in 500/502/403 after refresh
|
||||||
|
if [[ " ${all_errors[@]} " =~ " ${status_code} " ]]; then
|
||||||
|
echo "Error: Received $status_code for ID $video_id after token refresh."
|
||||||
|
else
|
||||||
|
echo "Token refresh succeeded for ID $video_id."
|
||||||
|
fi
|
||||||
|
elif [ "$status_code" -eq 304 ]; then
|
||||||
|
echo "Content is still fresh for ID $video_id. No action required."
|
||||||
|
else
|
||||||
|
echo "we are so barack (Status code for ID $video_id is neither 502, 500, nor 403.)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "----------------****************----------------" # Separator for readability
|
||||||
|
done
|
||||||
|
|
||||||
|
# If all videos still resulted in 500/502/403 errors even after running inv-update-token.sh, try restaring
|
||||||
|
if [ "$error_count" -eq "${#shuffled_video_ids[@]}" ]; then
|
||||||
|
echo "All videos failed to load after running inv-update-token.sh. Restarting services..."
|
||||||
|
restart_services
|
||||||
|
fi
|
10
backend-services/services/yt-block-protect.service
Normal file
10
backend-services/services/yt-block-protect.service
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=YouTube anti-block
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/home/qt/globe/scripts/inv-restart-docker.sh
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
10
backend-services/services/yt-block-protect.timer
Normal file
10
backend-services/services/yt-block-protect.timer
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Make yt anti-block Run every Minute
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnUnitActiveSec=1min
|
||||||
|
Unit=yt-block-protect.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
|
19
config.json
Normal file
19
config.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"tubeApi": "https://inner-api.poketube.fun/api/",
|
||||||
|
"invapi": "https://invid-api.poketube.fun/bHj665PpYhUdPWuKPfZuQGoX/api/v1",
|
||||||
|
"dislikes": "https://returnyoutubedislikeapi.com/votes?videoId=",
|
||||||
|
"invchannel": "https://invid-api.poketube.fun/api/v1",
|
||||||
|
"p_url":"https://p.poketube.fun",
|
||||||
|
"media_proxy": "https://image-proxy.poketube.fun",
|
||||||
|
"videourl":"https://eu-proxy.poketube.fun",
|
||||||
|
"email_main_url":"https://email-server.poketube.fun",
|
||||||
|
"mastodon_client_url":"https://social.poketube.fun",
|
||||||
|
"mastodon_client_server_name":"PokeSocial",
|
||||||
|
"libreoffice_online_url":"https://office.poketube.fun",
|
||||||
|
"cacher_max_age": "86400",
|
||||||
|
"enablealwayshttps": false,
|
||||||
|
"proxylocation":"USA",
|
||||||
|
"banner":"welcome to poke!",
|
||||||
|
"t_url": "https://t.poketube.fun/",
|
||||||
|
"server_port": "6003"
|
||||||
|
}
|
16
core/InnerTube/CacheItem.cs
Normal file
16
core/InnerTube/CacheItem.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace InnerTube
|
||||||
|
{
|
||||||
|
public class CacheItem<T>
|
||||||
|
{
|
||||||
|
public T Item;
|
||||||
|
public DateTimeOffset ExpireTime;
|
||||||
|
|
||||||
|
public CacheItem(T item, TimeSpan expiresIn)
|
||||||
|
{
|
||||||
|
Item = item;
|
||||||
|
ExpireTime = DateTimeOffset.Now.Add(expiresIn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
core/InnerTube/Enums.cs
Normal file
12
core/InnerTube/Enums.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace InnerTube
|
||||||
|
{
|
||||||
|
public enum ChannelTabs
|
||||||
|
{
|
||||||
|
Home,
|
||||||
|
Videos,
|
||||||
|
Playlists,
|
||||||
|
Community,
|
||||||
|
Channels,
|
||||||
|
About
|
||||||
|
}
|
||||||
|
}
|
11
core/InnerTube/InnerTube.csproj
Normal file
11
core/InnerTube/InnerTube.csproj
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
380
core/InnerTube/Models/DynamicItem.cs
Normal file
380
core/InnerTube/Models/DynamicItem.cs
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
using System;
|
||||||
|
using System.Xml;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace InnerTube.Models
|
||||||
|
{
|
||||||
|
public class DynamicItem
|
||||||
|
{
|
||||||
|
public string Id;
|
||||||
|
public string Title;
|
||||||
|
public Thumbnail[] Thumbnails;
|
||||||
|
|
||||||
|
public virtual XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("DynamicItem");
|
||||||
|
item.SetAttribute("id", Id);
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Title");
|
||||||
|
title.InnerText = Title;
|
||||||
|
item.AppendChild(title);
|
||||||
|
|
||||||
|
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
item.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VideoItem : DynamicItem
|
||||||
|
{
|
||||||
|
public string UploadedAt;
|
||||||
|
public long Views;
|
||||||
|
public Channel Channel;
|
||||||
|
public string Duration;
|
||||||
|
public string Description;
|
||||||
|
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("Video");
|
||||||
|
item.SetAttribute("id", Id);
|
||||||
|
item.SetAttribute("duration", Duration);
|
||||||
|
item.SetAttribute("views", Views.ToString());
|
||||||
|
item.SetAttribute("uploadedAt", UploadedAt);
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Title");
|
||||||
|
title.InnerText = Title;
|
||||||
|
item.AppendChild(title);
|
||||||
|
if (Channel is not null)
|
||||||
|
item.AppendChild(Channel.GetXmlElement(doc));
|
||||||
|
|
||||||
|
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
item.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(Description))
|
||||||
|
{
|
||||||
|
XmlElement description = doc.CreateElement("Description");
|
||||||
|
description.InnerText = Description;
|
||||||
|
item.AppendChild(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlaylistItem : DynamicItem
|
||||||
|
{
|
||||||
|
public int VideoCount;
|
||||||
|
public string FirstVideoId;
|
||||||
|
public Channel Channel;
|
||||||
|
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("Playlist");
|
||||||
|
item.SetAttribute("id", Id);
|
||||||
|
item.SetAttribute("videoCount", VideoCount.ToString());
|
||||||
|
item.SetAttribute("firstVideoId", FirstVideoId);
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Title");
|
||||||
|
title.InnerText = Title;
|
||||||
|
item.AppendChild(title);
|
||||||
|
item.AppendChild(Channel.GetXmlElement(doc));
|
||||||
|
|
||||||
|
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
item.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RadioItem : DynamicItem
|
||||||
|
{
|
||||||
|
public string FirstVideoId;
|
||||||
|
public Channel Channel;
|
||||||
|
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("Radio");
|
||||||
|
item.SetAttribute("id", Id);
|
||||||
|
item.SetAttribute("firstVideoId", FirstVideoId);
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Title");
|
||||||
|
title.InnerText = Title;
|
||||||
|
item.AppendChild(title);
|
||||||
|
item.AppendChild(Channel.GetXmlElement(doc));
|
||||||
|
|
||||||
|
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
item.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChannelItem : DynamicItem
|
||||||
|
{
|
||||||
|
public string Url;
|
||||||
|
public string Description;
|
||||||
|
public long VideoCount;
|
||||||
|
public string Subscribers;
|
||||||
|
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("Channel");
|
||||||
|
item.SetAttribute("id", Id);
|
||||||
|
item.SetAttribute("videoCount", VideoCount.ToString());
|
||||||
|
item.SetAttribute("subscribers", Subscribers);
|
||||||
|
if (!string.IsNullOrWhiteSpace(Url))
|
||||||
|
item.SetAttribute("customUrl", Url);
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Name");
|
||||||
|
title.InnerText = Title;
|
||||||
|
item.AppendChild(title);
|
||||||
|
|
||||||
|
XmlElement description = doc.CreateElement("Description");
|
||||||
|
description.InnerText = Description;
|
||||||
|
item.AppendChild(description);
|
||||||
|
|
||||||
|
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Avatar");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
item.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ContinuationItem : DynamicItem
|
||||||
|
{
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("Continuation");
|
||||||
|
item.SetAttribute("key", Id);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ShelfItem : DynamicItem
|
||||||
|
{
|
||||||
|
public DynamicItem[] Items;
|
||||||
|
public int CollapsedItemCount;
|
||||||
|
public BadgeItem[] Badges;
|
||||||
|
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("Shelf");
|
||||||
|
item.SetAttribute("title", Title);
|
||||||
|
item.SetAttribute("collapsedItemCount", CollapsedItemCount.ToString());
|
||||||
|
|
||||||
|
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
item.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Badges.Length > 0)
|
||||||
|
{
|
||||||
|
XmlElement badges = doc.CreateElement("Badges");
|
||||||
|
foreach (BadgeItem badge in Badges) badges.AppendChild(badge.GetXmlElement(doc));
|
||||||
|
item.AppendChild(badges);
|
||||||
|
}
|
||||||
|
|
||||||
|
XmlElement items = doc.CreateElement("Items");
|
||||||
|
foreach (DynamicItem dynamicItem in Items) items.AppendChild(dynamicItem.GetXmlElement(doc));
|
||||||
|
item.AppendChild(items);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HorizontalCardListItem : DynamicItem
|
||||||
|
{
|
||||||
|
public DynamicItem[] Items;
|
||||||
|
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("CardList");
|
||||||
|
item.SetAttribute("title", Title);
|
||||||
|
|
||||||
|
foreach (DynamicItem dynamicItem in Items) item.AppendChild(dynamicItem.GetXmlElement(doc));
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CardItem : DynamicItem
|
||||||
|
{
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("Card");
|
||||||
|
item.SetAttribute("title", Title);
|
||||||
|
|
||||||
|
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
item.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlaylistVideoItem : DynamicItem
|
||||||
|
{
|
||||||
|
public long Index;
|
||||||
|
public Channel Channel;
|
||||||
|
public string Duration;
|
||||||
|
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("Video");
|
||||||
|
item.SetAttribute("id", Id);
|
||||||
|
item.SetAttribute("index", Index.ToString());
|
||||||
|
item.SetAttribute("duration", Duration);
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Title");
|
||||||
|
title.InnerText = Title;
|
||||||
|
item.AppendChild(title);
|
||||||
|
item.AppendChild(Channel.GetXmlElement(doc));
|
||||||
|
|
||||||
|
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
item.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ItemSectionItem : DynamicItem
|
||||||
|
{
|
||||||
|
public DynamicItem[] Contents;
|
||||||
|
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement section = doc.CreateElement("ItemSection");
|
||||||
|
foreach (DynamicItem item in Contents) section.AppendChild(item.GetXmlElement(doc));
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MessageItem : DynamicItem
|
||||||
|
{
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement message = doc.CreateElement("Message");
|
||||||
|
message.InnerText = Title;
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChannelAboutItem : DynamicItem
|
||||||
|
{
|
||||||
|
public string Description;
|
||||||
|
public string Country;
|
||||||
|
public string Joined;
|
||||||
|
public string ViewCount;
|
||||||
|
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement about = doc.CreateElement("About");
|
||||||
|
XmlElement description = doc.CreateElement("Description");
|
||||||
|
description.InnerText = Description;
|
||||||
|
about.AppendChild(description);
|
||||||
|
XmlElement country = doc.CreateElement("Location");
|
||||||
|
country.InnerText = Country;
|
||||||
|
about.AppendChild(country);
|
||||||
|
XmlElement joined = doc.CreateElement("Joined");
|
||||||
|
joined.InnerText = Joined;
|
||||||
|
about.AppendChild(joined);
|
||||||
|
XmlElement viewCount = doc.CreateElement("ViewCount");
|
||||||
|
viewCount.InnerText = ViewCount;
|
||||||
|
about.AppendChild(viewCount);
|
||||||
|
return about;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BadgeItem : DynamicItem
|
||||||
|
{
|
||||||
|
public string Style;
|
||||||
|
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement badge = doc.CreateElement("Badge");
|
||||||
|
badge.SetAttribute("style", Style);
|
||||||
|
badge.InnerText = Title;
|
||||||
|
return badge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StationItem : DynamicItem
|
||||||
|
{
|
||||||
|
public int VideoCount;
|
||||||
|
public string FirstVideoId;
|
||||||
|
public string Description;
|
||||||
|
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("Station");
|
||||||
|
item.SetAttribute("id", Id);
|
||||||
|
item.SetAttribute("videoCount", VideoCount.ToString());
|
||||||
|
item.SetAttribute("firstVideoId", FirstVideoId);
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Title");
|
||||||
|
title.InnerText = Title;
|
||||||
|
item.AppendChild(title);
|
||||||
|
|
||||||
|
XmlElement description = doc.CreateElement("Description");
|
||||||
|
description.InnerText = Description;
|
||||||
|
item.AppendChild(description);
|
||||||
|
|
||||||
|
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
item.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
67
core/InnerTube/Models/RequestContext.cs
Normal file
67
core/InnerTube/Models/RequestContext.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace InnerTube.Models
|
||||||
|
{
|
||||||
|
public class RequestContext
|
||||||
|
{
|
||||||
|
[JsonProperty("context")] public Context Context;
|
||||||
|
|
||||||
|
public static string BuildRequestContextJson(Dictionary<string, object> additionalFields, string language = "en",
|
||||||
|
string region = "US", string clientName = "WEB", string clientVersion = "2.20220224.07.00")
|
||||||
|
{
|
||||||
|
RequestContext ctx = new()
|
||||||
|
{
|
||||||
|
Context = new Context(
|
||||||
|
new RequestClient(language, region, clientName, clientVersion),
|
||||||
|
new RequestUser(false))
|
||||||
|
};
|
||||||
|
|
||||||
|
string json1 = JsonConvert.SerializeObject(ctx);
|
||||||
|
Dictionary<string, object> json2 = JsonConvert.DeserializeObject<Dictionary<string, object>>(json1);
|
||||||
|
foreach (KeyValuePair<string,object> pair in additionalFields) json2.Add(pair.Key, pair.Value);
|
||||||
|
|
||||||
|
return JsonConvert.SerializeObject(json2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Context
|
||||||
|
{
|
||||||
|
[JsonProperty("client")] public RequestClient RequestClient { get; set; }
|
||||||
|
[JsonProperty("user")] public RequestUser RequestUser { get; set; }
|
||||||
|
|
||||||
|
public Context(RequestClient requestClient, RequestUser requestUser)
|
||||||
|
{
|
||||||
|
RequestClient = requestClient;
|
||||||
|
RequestUser = requestUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RequestClient
|
||||||
|
{
|
||||||
|
[JsonProperty("hl")] public string Language { get; set; }
|
||||||
|
[JsonProperty("gl")] public string Region { get; set; }
|
||||||
|
[JsonProperty("clientName")] public string ClientName { get; set; }
|
||||||
|
[JsonProperty("clientVersion")] public string ClientVersion { get; set; }
|
||||||
|
[JsonProperty("deviceModel")] public string DeviceModel { get; set; }
|
||||||
|
|
||||||
|
public RequestClient(string language, string region, string clientName, string clientVersion)
|
||||||
|
{
|
||||||
|
Language = language;
|
||||||
|
Region = region;
|
||||||
|
ClientName = clientName;
|
||||||
|
ClientVersion = clientVersion;
|
||||||
|
if (clientName == "IOS") DeviceModel = "iPhone14,3";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RequestUser
|
||||||
|
{
|
||||||
|
[JsonProperty("lockedSafetyMode")] public bool LockedSafetyMode { get; set; }
|
||||||
|
|
||||||
|
public RequestUser(bool lockedSafetyMode)
|
||||||
|
{
|
||||||
|
LockedSafetyMode = lockedSafetyMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
core/InnerTube/Models/YoutubeChannel.cs
Normal file
71
core/InnerTube/Models/YoutubeChannel.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
using System.Xml;
|
||||||
|
|
||||||
|
namespace InnerTube.Models
|
||||||
|
{
|
||||||
|
public class YoutubeChannel
|
||||||
|
{
|
||||||
|
public string Id;
|
||||||
|
public string Name;
|
||||||
|
public string Url;
|
||||||
|
public Thumbnail[] Avatars;
|
||||||
|
public Thumbnail[] Banners;
|
||||||
|
public string Description;
|
||||||
|
public DynamicItem[] Videos;
|
||||||
|
public string Subscribers;
|
||||||
|
|
||||||
|
public string GetHtmlDescription()
|
||||||
|
{
|
||||||
|
return Utils.GetHtmlDescription(Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
public XmlDocument GetXmlDocument()
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement channel = doc.CreateElement("Channel");
|
||||||
|
channel.SetAttribute("id", Id);
|
||||||
|
if (Id != Url)
|
||||||
|
channel.SetAttribute("customUrl", Url);
|
||||||
|
|
||||||
|
XmlElement metadata = doc.CreateElement("Metadata");
|
||||||
|
|
||||||
|
XmlElement name = doc.CreateElement("Name");
|
||||||
|
name.InnerText = Name;
|
||||||
|
metadata.AppendChild(name);
|
||||||
|
|
||||||
|
XmlElement avatars = doc.CreateElement("Avatars");
|
||||||
|
foreach (Thumbnail t in Avatars)
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
avatars.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
metadata.AppendChild(avatars);
|
||||||
|
|
||||||
|
XmlElement banners = doc.CreateElement("Banners");
|
||||||
|
foreach (Thumbnail t in Banners)
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
banners.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
metadata.AppendChild(banners);
|
||||||
|
|
||||||
|
XmlElement subscriberCount = doc.CreateElement("Subscribers");
|
||||||
|
subscriberCount.InnerText = Subscribers;
|
||||||
|
metadata.AppendChild(subscriberCount);
|
||||||
|
|
||||||
|
channel.AppendChild(metadata);
|
||||||
|
|
||||||
|
XmlElement contents = doc.CreateElement("Contents");
|
||||||
|
foreach (DynamicItem item in Videos) contents.AppendChild(item.GetXmlElement(doc));
|
||||||
|
channel.AppendChild(contents);
|
||||||
|
|
||||||
|
doc.AppendChild(channel);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
core/InnerTube/Models/YoutubeLocals.cs
Normal file
40
core/InnerTube/Models/YoutubeLocals.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Xml;
|
||||||
|
|
||||||
|
namespace InnerTube.Models
|
||||||
|
{
|
||||||
|
public class YoutubeLocals
|
||||||
|
{
|
||||||
|
public Dictionary<string, string> Languages { get; set; }
|
||||||
|
public Dictionary<string, string> Regions { get; set; }
|
||||||
|
|
||||||
|
public XmlDocument GetXmlDocument()
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement locals = doc.CreateElement("Locals");
|
||||||
|
|
||||||
|
XmlElement languages = doc.CreateElement("Languages");
|
||||||
|
foreach (KeyValuePair<string, string> l in Languages)
|
||||||
|
{
|
||||||
|
XmlElement language = doc.CreateElement("Language");
|
||||||
|
language.SetAttribute("hl", l.Key);
|
||||||
|
language.InnerText = l.Value;
|
||||||
|
languages.AppendChild(language);
|
||||||
|
}
|
||||||
|
locals.AppendChild(languages);
|
||||||
|
|
||||||
|
XmlElement regions = doc.CreateElement("Regions");
|
||||||
|
foreach (KeyValuePair<string, string> r in Regions)
|
||||||
|
{
|
||||||
|
XmlElement region = doc.CreateElement("Region");
|
||||||
|
region.SetAttribute("gl", r.Key);
|
||||||
|
region.InnerText = r.Value;
|
||||||
|
regions.AppendChild(region);
|
||||||
|
}
|
||||||
|
locals.AppendChild(regions);
|
||||||
|
|
||||||
|
doc.AppendChild(locals);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
230
core/InnerTube/Models/YoutubePlayer.cs
Normal file
230
core/InnerTube/Models/YoutubePlayer.cs
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
using System;
|
||||||
|
using System.Xml;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace InnerTube.Models
|
||||||
|
{
|
||||||
|
public class YoutubePlayer
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string[] Tags { get; set; }
|
||||||
|
public Channel Channel { get; set; }
|
||||||
|
public long? Duration { get; set; }
|
||||||
|
public bool IsLive { get; set; }
|
||||||
|
public Chapter[] Chapters { get; set; }
|
||||||
|
public Thumbnail[] Thumbnails { get; set; }
|
||||||
|
public Format[] Formats { get; set; }
|
||||||
|
public Format[] AdaptiveFormats { get; set; }
|
||||||
|
public string HlsManifestUrl { get; set; }
|
||||||
|
public Subtitle[] Subtitles { get; set; }
|
||||||
|
public string[] Storyboards { get; set; }
|
||||||
|
public string ExpiresInSeconds { get; set; }
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
public string GetHtmlDescription()
|
||||||
|
{
|
||||||
|
return Utils.GetHtmlDescription(Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
public XmlDocument GetXmlDocument()
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
||||||
|
{
|
||||||
|
XmlElement error = doc.CreateElement("Error");
|
||||||
|
error.InnerText = ErrorMessage;
|
||||||
|
doc.AppendChild(error);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
XmlElement player = doc.CreateElement("Player");
|
||||||
|
player.SetAttribute("id", Id);
|
||||||
|
player.SetAttribute("duration", Duration.ToString());
|
||||||
|
player.SetAttribute("isLive", IsLive.ToString());
|
||||||
|
player.SetAttribute("expiresInSeconds", ExpiresInSeconds);
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Title");
|
||||||
|
title.InnerText = Title;
|
||||||
|
player.AppendChild(title);
|
||||||
|
|
||||||
|
XmlElement description = doc.CreateElement("Description");
|
||||||
|
description.InnerText = Description;
|
||||||
|
player.AppendChild(description);
|
||||||
|
|
||||||
|
XmlElement tags = doc.CreateElement("Tags");
|
||||||
|
foreach (string tag in Tags ?? Array.Empty<string>())
|
||||||
|
{
|
||||||
|
XmlElement tagElement = doc.CreateElement("Tag");
|
||||||
|
tagElement.InnerText = tag;
|
||||||
|
tags.AppendChild(tagElement);
|
||||||
|
}
|
||||||
|
player.AppendChild(tags);
|
||||||
|
|
||||||
|
player.AppendChild(Channel.GetXmlElement(doc));
|
||||||
|
|
||||||
|
XmlElement thumbnails = doc.CreateElement("Thumbnails");
|
||||||
|
foreach (Thumbnail t in Thumbnails)
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
thumbnails.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
player.AppendChild(thumbnails);
|
||||||
|
|
||||||
|
XmlElement formats = doc.CreateElement("Formats");
|
||||||
|
foreach (Format f in Formats ?? Array.Empty<Format>()) formats.AppendChild(f.GetXmlElement(doc));
|
||||||
|
player.AppendChild(formats);
|
||||||
|
|
||||||
|
XmlElement adaptiveFormats = doc.CreateElement("AdaptiveFormats");
|
||||||
|
foreach (Format f in AdaptiveFormats ?? Array.Empty<Format>()) adaptiveFormats.AppendChild(f.GetXmlElement(doc));
|
||||||
|
player.AppendChild(adaptiveFormats);
|
||||||
|
|
||||||
|
XmlElement storyboards = doc.CreateElement("Storyboards");
|
||||||
|
foreach (string s in Storyboards)
|
||||||
|
{
|
||||||
|
XmlElement storyboard = doc.CreateElement("Storyboard");
|
||||||
|
storyboard.InnerText = s;
|
||||||
|
storyboards.AppendChild(storyboard);
|
||||||
|
}
|
||||||
|
player.AppendChild(storyboards);
|
||||||
|
|
||||||
|
XmlElement subtitles = doc.CreateElement("Subtitles");
|
||||||
|
foreach (Subtitle s in Subtitles ?? Array.Empty<Subtitle>()) subtitles.AppendChild(s.GetXmlElement(doc));
|
||||||
|
player.AppendChild(subtitles);
|
||||||
|
|
||||||
|
doc.AppendChild(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Chapter
|
||||||
|
{
|
||||||
|
[JsonProperty("title")] public string Title { get; set; }
|
||||||
|
[JsonProperty("start_time")] public long StartTime { get; set; }
|
||||||
|
[JsonProperty("end_time")] public long EndTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Format
|
||||||
|
{
|
||||||
|
[JsonProperty("format")] public string FormatName { get; set; }
|
||||||
|
[JsonProperty("format_id")] public string FormatId { get; set; }
|
||||||
|
[JsonProperty("format_note")] public string FormatNote { get; set; }
|
||||||
|
[JsonProperty("filesize")] public long? Filesize { get; set; }
|
||||||
|
[JsonProperty("quality")] public long Quality { get; set; }
|
||||||
|
[JsonProperty("bitrate")] public double Bitrate { get; set; }
|
||||||
|
[JsonProperty("audio_codec")] public string AudioCodec { get; set; }
|
||||||
|
[JsonProperty("video_codec")] public string VideoCodec { get; set; }
|
||||||
|
[JsonProperty("audio_sample_rate")] public long? AudioSampleRate { get; set; }
|
||||||
|
[JsonProperty("resolution")] public string Resolution { get; set; }
|
||||||
|
[JsonProperty("url")] public string Url { get; set; }
|
||||||
|
[JsonProperty("init_range")] public Range InitRange { get; set; }
|
||||||
|
[JsonProperty("index_range")] public Range IndexRange { get; set; }
|
||||||
|
|
||||||
|
public XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement format = doc.CreateElement("Format");
|
||||||
|
|
||||||
|
format.SetAttribute("id", FormatId);
|
||||||
|
format.SetAttribute("label", FormatName);
|
||||||
|
format.SetAttribute("filesize", Filesize.ToString());
|
||||||
|
format.SetAttribute("quality", Bitrate.ToString());
|
||||||
|
format.SetAttribute("audioCodec", AudioCodec);
|
||||||
|
format.SetAttribute("videoCodec", VideoCodec);
|
||||||
|
if (AudioSampleRate != null)
|
||||||
|
format.SetAttribute("audioSampleRate", AudioSampleRate.ToString());
|
||||||
|
else
|
||||||
|
format.SetAttribute("resolution", Resolution);
|
||||||
|
|
||||||
|
XmlElement url = doc.CreateElement("URL");
|
||||||
|
url.InnerText = Url;
|
||||||
|
format.AppendChild(url);
|
||||||
|
|
||||||
|
if (InitRange != null && IndexRange != null)
|
||||||
|
{
|
||||||
|
XmlElement initRange = doc.CreateElement("InitRange");
|
||||||
|
initRange.SetAttribute("start", InitRange.Start);
|
||||||
|
initRange.SetAttribute("end", InitRange.End);
|
||||||
|
format.AppendChild(initRange);
|
||||||
|
|
||||||
|
XmlElement indexRange = doc.CreateElement("IndexRange");
|
||||||
|
indexRange.SetAttribute("start", IndexRange.Start);
|
||||||
|
indexRange.SetAttribute("end", IndexRange.End);
|
||||||
|
format.AppendChild(indexRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Range
|
||||||
|
{
|
||||||
|
[JsonProperty("start")] public string Start { get; set; }
|
||||||
|
[JsonProperty("end")] public string End { get; set; }
|
||||||
|
|
||||||
|
public Range(string start, string end)
|
||||||
|
{
|
||||||
|
Start = start;
|
||||||
|
End = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Channel
|
||||||
|
{
|
||||||
|
[JsonProperty("name")] public string Name { get; set; }
|
||||||
|
[JsonProperty("id")] public string Id { get; set; }
|
||||||
|
[JsonProperty("subscriberCount")] public string SubscriberCount { get; set; }
|
||||||
|
[JsonProperty("avatars")] public Thumbnail[] Avatars { get; set; }
|
||||||
|
|
||||||
|
public XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement channel = doc.CreateElement("Channel");
|
||||||
|
channel.SetAttribute("id", Id);
|
||||||
|
if (!string.IsNullOrWhiteSpace(SubscriberCount))
|
||||||
|
channel.SetAttribute("subscriberCount", SubscriberCount);
|
||||||
|
|
||||||
|
XmlElement name = doc.CreateElement("Name");
|
||||||
|
name.InnerText = Name;
|
||||||
|
channel.AppendChild(name);
|
||||||
|
|
||||||
|
foreach (Thumbnail avatarThumb in Avatars ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement avatar = doc.CreateElement("Avatar");
|
||||||
|
avatar.SetAttribute("width", avatarThumb.Width.ToString());
|
||||||
|
avatar.SetAttribute("height", avatarThumb.Height.ToString());
|
||||||
|
avatar.InnerText = avatarThumb.Url;
|
||||||
|
channel.AppendChild(avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Subtitle
|
||||||
|
{
|
||||||
|
[JsonProperty("ext")] public string Ext { get; set; }
|
||||||
|
[JsonProperty("name")] public string Language { get; set; }
|
||||||
|
[JsonProperty("url")] public string Url { get; set; }
|
||||||
|
|
||||||
|
public XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement subtitle = doc.CreateElement("Subtitle");
|
||||||
|
subtitle.SetAttribute("ext", Ext);
|
||||||
|
subtitle.SetAttribute("language", Language);
|
||||||
|
subtitle.InnerText = Url;
|
||||||
|
return subtitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Thumbnail
|
||||||
|
{
|
||||||
|
[JsonProperty("height")] public long Height { get; set; }
|
||||||
|
[JsonProperty("url")] public string Url { get; set; }
|
||||||
|
[JsonProperty("width")] public long Width { get; set; }
|
||||||
|
}
|
||||||
|
}
|
68
core/InnerTube/Models/YoutubePlaylist.cs
Normal file
68
core/InnerTube/Models/YoutubePlaylist.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
using System.Xml;
|
||||||
|
|
||||||
|
namespace InnerTube.Models
|
||||||
|
{
|
||||||
|
public class YoutubePlaylist
|
||||||
|
{
|
||||||
|
public string Id;
|
||||||
|
public string Title;
|
||||||
|
public string Description;
|
||||||
|
public string VideoCount;
|
||||||
|
public string ViewCount;
|
||||||
|
public string LastUpdated;
|
||||||
|
public Thumbnail[] Thumbnail;
|
||||||
|
public Channel Channel;
|
||||||
|
public DynamicItem[] Videos;
|
||||||
|
public string ContinuationKey;
|
||||||
|
|
||||||
|
public string GetHtmlDescription() => Utils.GetHtmlDescription(Description);
|
||||||
|
|
||||||
|
public XmlDocument GetXmlDocument()
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement playlist = doc.CreateElement("Playlist");
|
||||||
|
playlist.SetAttribute("id", Id);
|
||||||
|
playlist.SetAttribute("continuation", ContinuationKey);
|
||||||
|
|
||||||
|
XmlElement metadata = doc.CreateElement("Metadata");
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Title");
|
||||||
|
title.InnerText = Title;
|
||||||
|
metadata.AppendChild(title);
|
||||||
|
|
||||||
|
metadata.AppendChild(Channel.GetXmlElement(doc));
|
||||||
|
|
||||||
|
XmlElement thumbnails = doc.CreateElement("Thumbnails");
|
||||||
|
foreach (Thumbnail t in Thumbnail)
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
thumbnails.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
metadata.AppendChild(thumbnails);
|
||||||
|
|
||||||
|
XmlElement videoCount = doc.CreateElement("VideoCount");
|
||||||
|
XmlElement viewCount = doc.CreateElement("ViewCount");
|
||||||
|
XmlElement lastUpdated = doc.CreateElement("LastUpdated");
|
||||||
|
|
||||||
|
videoCount.InnerText = VideoCount;
|
||||||
|
viewCount.InnerText = ViewCount;
|
||||||
|
lastUpdated.InnerText = LastUpdated;
|
||||||
|
|
||||||
|
metadata.AppendChild(videoCount);
|
||||||
|
metadata.AppendChild(viewCount);
|
||||||
|
metadata.AppendChild(lastUpdated);
|
||||||
|
|
||||||
|
playlist.AppendChild(metadata);
|
||||||
|
|
||||||
|
XmlElement results = doc.CreateElement("Videos");
|
||||||
|
foreach (DynamicItem result in Videos) results.AppendChild(result.GetXmlElement(doc));
|
||||||
|
playlist.AppendChild(results);
|
||||||
|
|
||||||
|
doc.AppendChild(playlist);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
core/InnerTube/Models/YoutubeSearchResults.cs
Normal file
39
core/InnerTube/Models/YoutubeSearchResults.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using System.Xml;
|
||||||
|
|
||||||
|
namespace InnerTube.Models
|
||||||
|
{
|
||||||
|
public class YoutubeSearchResults
|
||||||
|
{
|
||||||
|
public string[] Refinements;
|
||||||
|
public long EstimatedResults;
|
||||||
|
public DynamicItem[] Results;
|
||||||
|
public string ContinuationKey;
|
||||||
|
|
||||||
|
public XmlDocument GetXmlDocument()
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement search = doc.CreateElement("Search");
|
||||||
|
search.SetAttribute("estimatedResults", EstimatedResults.ToString());
|
||||||
|
search.SetAttribute("continuation", ContinuationKey);
|
||||||
|
|
||||||
|
if (Refinements.Length > 0)
|
||||||
|
{
|
||||||
|
XmlElement refinements = doc.CreateElement("Refinements");
|
||||||
|
foreach (string refinementText in Refinements)
|
||||||
|
{
|
||||||
|
XmlElement refinement = doc.CreateElement("Refinement");
|
||||||
|
refinement.InnerText = refinementText;
|
||||||
|
refinements.AppendChild(refinement);
|
||||||
|
}
|
||||||
|
search.AppendChild(refinements);
|
||||||
|
}
|
||||||
|
|
||||||
|
XmlElement results = doc.CreateElement("Results");
|
||||||
|
foreach (DynamicItem result in Results) results.AppendChild(result.GetXmlElement(doc));
|
||||||
|
search.AppendChild(results);
|
||||||
|
|
||||||
|
doc.AppendChild(search);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
core/InnerTube/Models/YoutubeStoryboardSpec.cs
Normal file
38
core/InnerTube/Models/YoutubeStoryboardSpec.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace InnerTube.Models
|
||||||
|
{
|
||||||
|
public class YoutubeStoryboardSpec
|
||||||
|
{
|
||||||
|
public Dictionary<string, string> Urls = new();
|
||||||
|
public YoutubeStoryboardSpec(string specStr, long duration)
|
||||||
|
{
|
||||||
|
if (specStr is null) return;
|
||||||
|
List<string> spec = new(specStr.Split("|"));
|
||||||
|
string baseUrl = spec[0];
|
||||||
|
spec.RemoveAt(0);
|
||||||
|
spec.Reverse();
|
||||||
|
int L = spec.Count - 1;
|
||||||
|
for (int i = 0; i < spec.Count; i++)
|
||||||
|
{
|
||||||
|
string[] args = spec[i].Split("#");
|
||||||
|
int width = int.Parse(args[0]);
|
||||||
|
int height = int.Parse(args[1]);
|
||||||
|
int frameCount = int.Parse(args[2]);
|
||||||
|
int cols = int.Parse(args[3]);
|
||||||
|
int rows = int.Parse(args[4]);
|
||||||
|
string N = args[6];
|
||||||
|
string sigh = args[7];
|
||||||
|
string url = baseUrl
|
||||||
|
.Replace("$L", (spec.Count - 1 - i).ToString())
|
||||||
|
.Replace("$N", N) + "&sigh=" + sigh;
|
||||||
|
float fragmentCount = frameCount / (cols * rows);
|
||||||
|
float fragmentDuration = duration / fragmentCount;
|
||||||
|
|
||||||
|
for (int j = 0; j < Math.Ceiling(fragmentCount); j++)
|
||||||
|
Urls.TryAdd($"L{spec.Count - 1 - i}", url.Replace("$M", j.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
core/InnerTube/Models/YoutubeTrends.cs
Normal file
70
core/InnerTube/Models/YoutubeTrends.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using System;
|
||||||
|
using System.Xml;
|
||||||
|
|
||||||
|
namespace InnerTube.Models
|
||||||
|
{
|
||||||
|
public class YoutubeTrends
|
||||||
|
{
|
||||||
|
public TrendCategory[] Categories;
|
||||||
|
public DynamicItem[] Videos;
|
||||||
|
|
||||||
|
public XmlDocument GetXmlDocument()
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement explore = doc.CreateElement("Explore");
|
||||||
|
|
||||||
|
XmlElement categories = doc.CreateElement("Categories");
|
||||||
|
foreach (TrendCategory category in Categories ?? Array.Empty<TrendCategory>()) categories.AppendChild(category.GetXmlElement(doc));
|
||||||
|
explore.AppendChild(categories);
|
||||||
|
|
||||||
|
XmlElement contents = doc.CreateElement("Videos");
|
||||||
|
foreach (DynamicItem item in Videos ?? Array.Empty<DynamicItem>()) contents.AppendChild(item.GetXmlElement(doc));
|
||||||
|
explore.AppendChild(contents);
|
||||||
|
|
||||||
|
doc.AppendChild(explore);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TrendCategory
|
||||||
|
{
|
||||||
|
public string Label;
|
||||||
|
public Thumbnail[] BackgroundImage;
|
||||||
|
public Thumbnail[] Icon;
|
||||||
|
public string Id;
|
||||||
|
|
||||||
|
public XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement category = doc.CreateElement("Category");
|
||||||
|
category.SetAttribute("id", Id);
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Name");
|
||||||
|
title.InnerText = Label;
|
||||||
|
category.AppendChild(title);
|
||||||
|
|
||||||
|
XmlElement backgroundImages = doc.CreateElement("BackgroundImage");
|
||||||
|
foreach (Thumbnail t in BackgroundImage ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
backgroundImages.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
category.AppendChild(backgroundImages);
|
||||||
|
|
||||||
|
XmlElement icons = doc.CreateElement("Icon");
|
||||||
|
foreach (Thumbnail t in Icon ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
icons.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
category.AppendChild(icons);
|
||||||
|
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
core/InnerTube/Models/YoutubeVideo.cs
Normal file
45
core/InnerTube/Models/YoutubeVideo.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Xml;
|
||||||
|
|
||||||
|
namespace InnerTube.Models
|
||||||
|
{
|
||||||
|
public class YoutubeVideo
|
||||||
|
{
|
||||||
|
public string Id;
|
||||||
|
public string Title;
|
||||||
|
public string Description;
|
||||||
|
public Channel Channel;
|
||||||
|
public string UploadDate;
|
||||||
|
public DynamicItem[] Recommended;
|
||||||
|
public string Views;
|
||||||
|
|
||||||
|
public string GetHtmlDescription() => InnerTube.Utils.GetHtmlDescription(Description);
|
||||||
|
|
||||||
|
public XmlDocument GetXmlDocument()
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement item = doc.CreateElement("Video");
|
||||||
|
|
||||||
|
item.SetAttribute("id", Id);
|
||||||
|
item.SetAttribute("views", Views);
|
||||||
|
item.SetAttribute("uploadDate", UploadDate);
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Title");
|
||||||
|
title.InnerText = Title;
|
||||||
|
item.AppendChild(title);
|
||||||
|
|
||||||
|
XmlElement description = doc.CreateElement("Description");
|
||||||
|
description.InnerText = Description;
|
||||||
|
item.AppendChild(description);
|
||||||
|
|
||||||
|
item.AppendChild(Channel.GetXmlElement(doc));
|
||||||
|
|
||||||
|
XmlElement recommendations = doc.CreateElement("Recommendations");
|
||||||
|
foreach (DynamicItem f in Recommended ?? Array.Empty<DynamicItem>()) recommendations.AppendChild(f.GetXmlElement(doc));
|
||||||
|
item.AppendChild(recommendations);
|
||||||
|
|
||||||
|
doc.AppendChild(item);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
core/InnerTube/ReturnYouTubeDislike.cs
Normal file
44
core/InnerTube/ReturnYouTubeDislike.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace InnerTube
|
||||||
|
{
|
||||||
|
public static class ReturnYouTubeDislike
|
||||||
|
{
|
||||||
|
private static HttpClient _client = new();
|
||||||
|
private static Dictionary<string, YoutubeDislikes> DislikesCache = new();
|
||||||
|
|
||||||
|
// TODO: better cache
|
||||||
|
public static async Task<YoutubeDislikes> GetDislikes(string videoId)
|
||||||
|
{
|
||||||
|
if (DislikesCache.ContainsKey(videoId))
|
||||||
|
return DislikesCache[videoId];
|
||||||
|
|
||||||
|
HttpResponseMessage response = await _client.GetAsync("https://returnyoutubedislikeapi.com/votes?videoId=" + videoId);
|
||||||
|
string json = await response.Content.ReadAsStringAsync();
|
||||||
|
YoutubeDislikes dislikes = JsonConvert.DeserializeObject<YoutubeDislikes>(json);
|
||||||
|
if (dislikes is not null)
|
||||||
|
DislikesCache.Add(videoId, dislikes);
|
||||||
|
return dislikes ?? new YoutubeDislikes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class YoutubeDislikes
|
||||||
|
{
|
||||||
|
[JsonProperty("id")] public string Id { get; set; }
|
||||||
|
[JsonProperty("dateCreated")] public string DateCreated { get; set; }
|
||||||
|
[JsonProperty("likes")] public long Likes { get; set; }
|
||||||
|
[JsonProperty("dislikes")] public long Dislikes { get; set; }
|
||||||
|
[JsonProperty("rating")] public double Rating { get; set; }
|
||||||
|
[JsonProperty("viewCount")] public long Views { get; set; }
|
||||||
|
[JsonProperty("deleted")] public bool Deleted { get; set; }
|
||||||
|
|
||||||
|
public float GetLikePercentage()
|
||||||
|
{
|
||||||
|
return Likes / (float)(Likes + Dislikes) * 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
457
core/InnerTube/Utils.cs
Normal file
457
core/InnerTube/Utils.cs
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using System.Xml;
|
||||||
|
using InnerTube.Models;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace InnerTube
|
||||||
|
{
|
||||||
|
public static class Utils
|
||||||
|
{
|
||||||
|
private static string Sapisid;
|
||||||
|
private static string Psid;
|
||||||
|
private static bool UseAuthorization;
|
||||||
|
|
||||||
|
public static string GetHtmlDescription(string description) => description?.Replace("\n", "<br>") ?? "";
|
||||||
|
|
||||||
|
public static string GetMpdManifest(this YoutubePlayer player, string proxyUrl, string videoCodec = null, string audioCodec = null)
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
|
||||||
|
XmlDeclaration xmlDeclaration = doc.CreateXmlDeclaration("1.0", "UTF-8", null);
|
||||||
|
XmlElement root = doc.DocumentElement;
|
||||||
|
doc.InsertBefore(xmlDeclaration, root);
|
||||||
|
|
||||||
|
XmlElement mpdRoot = doc.CreateElement(string.Empty, "MPD", string.Empty);
|
||||||
|
mpdRoot.SetAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
|
||||||
|
mpdRoot.SetAttribute("xmlns", "urn:mpeg:dash:schema:mpd:2011");
|
||||||
|
mpdRoot.SetAttribute("xsi:schemaLocation", "urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd");
|
||||||
|
//mpdRoot.SetAttribute("profiles", "urn:mpeg:dash:profile:isoff-on-demand:2011");
|
||||||
|
mpdRoot.SetAttribute("profiles", "urn:mpeg:dash:profile:isoff-main:2011");
|
||||||
|
mpdRoot.SetAttribute("type", "static");
|
||||||
|
mpdRoot.SetAttribute("minBufferTime", "PT1.500S");
|
||||||
|
TimeSpan durationTs = TimeSpan.FromMilliseconds(double.Parse(HttpUtility
|
||||||
|
.ParseQueryString(player.Formats.First().Url.Split("?")[1])
|
||||||
|
.Get("dur")?.Replace(".", "") ?? "0"));
|
||||||
|
StringBuilder duration = new("PT");
|
||||||
|
if (durationTs.TotalHours > 0)
|
||||||
|
duration.Append($"{durationTs.Hours}H");
|
||||||
|
if (durationTs.Minutes > 0)
|
||||||
|
duration.Append($"{durationTs.Minutes}M");
|
||||||
|
if (durationTs.Seconds > 0)
|
||||||
|
duration.Append(durationTs.Seconds);
|
||||||
|
mpdRoot.SetAttribute("mediaPresentationDuration", $"{duration}.{durationTs.Milliseconds}S");
|
||||||
|
doc.AppendChild(mpdRoot);
|
||||||
|
|
||||||
|
XmlElement period = doc.CreateElement("Period");
|
||||||
|
|
||||||
|
period.AppendChild(doc.CreateComment("Audio Adaptation Set"));
|
||||||
|
XmlElement audioAdaptationSet = doc.CreateElement("AdaptationSet");
|
||||||
|
List<Format> audios;
|
||||||
|
if (audioCodec != "all")
|
||||||
|
audios = player.AdaptiveFormats
|
||||||
|
.Where(x => x.AudioSampleRate.HasValue && x.FormatId != "17" &&
|
||||||
|
(audioCodec == null || x.AudioCodec.ToLower().Contains(audioCodec.ToLower())))
|
||||||
|
.GroupBy(x => x.FormatNote)
|
||||||
|
.Select(x => x.Last())
|
||||||
|
.ToList();
|
||||||
|
else
|
||||||
|
audios = player.AdaptiveFormats
|
||||||
|
.Where(x => x.AudioSampleRate.HasValue && x.FormatId != "17")
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
audioAdaptationSet.SetAttribute("mimeType",
|
||||||
|
HttpUtility.ParseQueryString(audios.First().Url.Split("?")[1]).Get("mime"));
|
||||||
|
audioAdaptationSet.SetAttribute("subsegmentAlignment", "true");
|
||||||
|
audioAdaptationSet.SetAttribute("contentType", "audio");
|
||||||
|
foreach (Format format in audios)
|
||||||
|
{
|
||||||
|
XmlElement representation = doc.CreateElement("Representation");
|
||||||
|
representation.SetAttribute("id", format.FormatId);
|
||||||
|
representation.SetAttribute("codecs", format.AudioCodec);
|
||||||
|
representation.SetAttribute("startWithSAP", "1");
|
||||||
|
representation.SetAttribute("bandwidth",
|
||||||
|
Math.Floor((format.Filesize ?? 1) / (double)player.Duration).ToString());
|
||||||
|
|
||||||
|
XmlElement audioChannelConfiguration = doc.CreateElement("AudioChannelConfiguration");
|
||||||
|
audioChannelConfiguration.SetAttribute("schemeIdUri",
|
||||||
|
"urn:mpeg:dash:23003:3:audio_channel_configuration:2011");
|
||||||
|
audioChannelConfiguration.SetAttribute("value", "2");
|
||||||
|
representation.AppendChild(audioChannelConfiguration);
|
||||||
|
|
||||||
|
XmlElement baseUrl = doc.CreateElement("BaseURL");
|
||||||
|
baseUrl.InnerText = string.IsNullOrWhiteSpace(proxyUrl) ? format.Url : $"{proxyUrl}media/{player.Id}/{format.FormatId}";
|
||||||
|
representation.AppendChild(baseUrl);
|
||||||
|
|
||||||
|
if (format.IndexRange != null && format.InitRange != null)
|
||||||
|
{
|
||||||
|
XmlElement segmentBase = doc.CreateElement("SegmentBase");
|
||||||
|
segmentBase.SetAttribute("indexRange", $"{format.IndexRange.Start}-{format.IndexRange.End}");
|
||||||
|
segmentBase.SetAttribute("indexRangeExact", "true");
|
||||||
|
|
||||||
|
XmlElement initialization = doc.CreateElement("Initialization");
|
||||||
|
initialization.SetAttribute("range", $"{format.InitRange.Start}-{format.InitRange.End}");
|
||||||
|
|
||||||
|
segmentBase.AppendChild(initialization);
|
||||||
|
representation.AppendChild(segmentBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
audioAdaptationSet.AppendChild(representation);
|
||||||
|
}
|
||||||
|
|
||||||
|
period.AppendChild(audioAdaptationSet);
|
||||||
|
|
||||||
|
period.AppendChild(doc.CreateComment("Video Adaptation Set"));
|
||||||
|
|
||||||
|
List<Format> videos;
|
||||||
|
if (videoCodec != "all")
|
||||||
|
videos = player.AdaptiveFormats.Where(x => !x.AudioSampleRate.HasValue && x.FormatId != "17" &&
|
||||||
|
(videoCodec == null || x.VideoCodec.ToLower()
|
||||||
|
.Contains(videoCodec.ToLower())))
|
||||||
|
.GroupBy(x => x.FormatNote)
|
||||||
|
.Select(x => x.Last())
|
||||||
|
.ToList();
|
||||||
|
else
|
||||||
|
videos = player.AdaptiveFormats.Where(x => x.Resolution != "audio only" && x.FormatId != "17").ToList();
|
||||||
|
|
||||||
|
|
||||||
|
XmlElement videoAdaptationSet = doc.CreateElement("AdaptationSet");
|
||||||
|
videoAdaptationSet.SetAttribute("mimeType",
|
||||||
|
HttpUtility.ParseQueryString(videos.FirstOrDefault()?.Url?.Split("?")[1] ?? "mime=video/mp4")
|
||||||
|
.Get("mime"));
|
||||||
|
videoAdaptationSet.SetAttribute("subsegmentAlignment", "true");
|
||||||
|
videoAdaptationSet.SetAttribute("contentType", "video");
|
||||||
|
|
||||||
|
foreach (Format format in videos)
|
||||||
|
{
|
||||||
|
XmlElement representation = doc.CreateElement("Representation");
|
||||||
|
representation.SetAttribute("id", format.FormatId);
|
||||||
|
representation.SetAttribute("codecs", format.VideoCodec);
|
||||||
|
representation.SetAttribute("startWithSAP", "1");
|
||||||
|
string[] widthAndHeight = format.Resolution.Split("x");
|
||||||
|
representation.SetAttribute("width", widthAndHeight[0]);
|
||||||
|
representation.SetAttribute("height", widthAndHeight[1]);
|
||||||
|
representation.SetAttribute("bandwidth",
|
||||||
|
Math.Floor((format.Filesize ?? 1) / (double)player.Duration).ToString());
|
||||||
|
|
||||||
|
XmlElement baseUrl = doc.CreateElement("BaseURL");
|
||||||
|
baseUrl.InnerText = string.IsNullOrWhiteSpace(proxyUrl) ? format.Url : $"{proxyUrl}media/{player.Id}/{format.FormatId}";
|
||||||
|
representation.AppendChild(baseUrl);
|
||||||
|
|
||||||
|
if (format.IndexRange != null && format.InitRange != null)
|
||||||
|
{
|
||||||
|
XmlElement segmentBase = doc.CreateElement("SegmentBase");
|
||||||
|
segmentBase.SetAttribute("indexRange", $"{format.IndexRange.Start}-{format.IndexRange.End}");
|
||||||
|
segmentBase.SetAttribute("indexRangeExact", "true");
|
||||||
|
|
||||||
|
XmlElement initialization = doc.CreateElement("Initialization");
|
||||||
|
initialization.SetAttribute("range", $"{format.InitRange.Start}-{format.InitRange.End}");
|
||||||
|
|
||||||
|
segmentBase.AppendChild(initialization);
|
||||||
|
representation.AppendChild(segmentBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
videoAdaptationSet.AppendChild(representation);
|
||||||
|
}
|
||||||
|
|
||||||
|
period.AppendChild(videoAdaptationSet);
|
||||||
|
|
||||||
|
period.AppendChild(doc.CreateComment("Subtitle Adaptation Sets"));
|
||||||
|
foreach (Subtitle subtitle in player.Subtitles ?? Array.Empty<Subtitle>())
|
||||||
|
{
|
||||||
|
period.AppendChild(doc.CreateComment(subtitle.Language));
|
||||||
|
XmlElement adaptationSet = doc.CreateElement("AdaptationSet");
|
||||||
|
adaptationSet.SetAttribute("mimeType", "text/vtt");
|
||||||
|
adaptationSet.SetAttribute("lang", subtitle.Language);
|
||||||
|
|
||||||
|
XmlElement representation = doc.CreateElement("Representation");
|
||||||
|
representation.SetAttribute("id", $"caption_{subtitle.Language.ToLower()}");
|
||||||
|
representation.SetAttribute("bandwidth", "256"); // ...why do we need this for a plaintext file
|
||||||
|
|
||||||
|
XmlElement baseUrl = doc.CreateElement("BaseURL");
|
||||||
|
string url = subtitle.Url;
|
||||||
|
url = url.Replace("fmt=srv3", "fmt=vtt");
|
||||||
|
baseUrl.InnerText = string.IsNullOrWhiteSpace(proxyUrl) ? url : $"{proxyUrl}caption/{player.Id}/{subtitle.Language}";
|
||||||
|
|
||||||
|
representation.AppendChild(baseUrl);
|
||||||
|
adaptationSet.AppendChild(representation);
|
||||||
|
period.AppendChild(adaptationSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
mpdRoot.AppendChild(period);
|
||||||
|
return doc.OuterXml.Replace(" schemaLocation=\"", " xsi:schemaLocation=\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> GetHlsManifest(this YoutubePlayer player, string proxyUrl)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.AppendLine("#EXTM3U");
|
||||||
|
sb.AppendLine("##Generated by LightTube");
|
||||||
|
sb.AppendLine("##Video ID: " + player.Id);
|
||||||
|
|
||||||
|
sb.AppendLine("#EXT-X-VERSION:7");
|
||||||
|
sb.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS");
|
||||||
|
|
||||||
|
string hls = await new HttpClient().GetStringAsync(player.HlsManifestUrl);
|
||||||
|
string[] hlsLines = hls.Split("\n");
|
||||||
|
foreach (string line in hlsLines)
|
||||||
|
{
|
||||||
|
if (line.StartsWith("#EXT-X-STREAM-INF:"))
|
||||||
|
sb.AppendLine(line);
|
||||||
|
if (line.StartsWith("http"))
|
||||||
|
{
|
||||||
|
Uri u = new(line);
|
||||||
|
sb.AppendLine($"{proxyUrl}/ytmanifest?path={HttpUtility.UrlEncode(u.PathAndQuery)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ReadRuns(JArray runs)
|
||||||
|
{
|
||||||
|
string str = "";
|
||||||
|
foreach (JToken runToken in runs ?? new JArray())
|
||||||
|
{
|
||||||
|
JObject run = runToken as JObject;
|
||||||
|
if (run is null) continue;
|
||||||
|
|
||||||
|
if (run.ContainsKey("bold"))
|
||||||
|
{
|
||||||
|
str += "<b>" + run["text"] + "</b>";
|
||||||
|
}
|
||||||
|
else if (run.ContainsKey("navigationEndpoint"))
|
||||||
|
{
|
||||||
|
if (run?["navigationEndpoint"]?["urlEndpoint"] is not null)
|
||||||
|
{
|
||||||
|
string url = run["navigationEndpoint"]?["urlEndpoint"]?["url"]?.ToString() ?? "";
|
||||||
|
if (url.StartsWith("https://www.youtube.com/redirect"))
|
||||||
|
{
|
||||||
|
NameValueCollection qsl = HttpUtility.ParseQueryString(url.Split("?")[1]);
|
||||||
|
url = qsl["url"] ?? qsl["q"];
|
||||||
|
}
|
||||||
|
|
||||||
|
str += $"<a href=\"{url}\">{run["text"]}</a>";
|
||||||
|
}
|
||||||
|
else if (run?["navigationEndpoint"]?["commandMetadata"] is not null)
|
||||||
|
{
|
||||||
|
string url = run["navigationEndpoint"]?["commandMetadata"]?["webCommandMetadata"]?["url"]
|
||||||
|
?.ToString() ?? "";
|
||||||
|
if (url.StartsWith("/"))
|
||||||
|
url = "https://youtube.com" + url;
|
||||||
|
str += $"<a href=\"{url}\">{run["text"]}</a>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
str += run["text"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Thumbnail ParseThumbnails(JToken arg) => new()
|
||||||
|
{
|
||||||
|
Height = arg["height"]?.ToObject<long>() ?? -1,
|
||||||
|
Url = arg["url"]?.ToString() ?? string.Empty,
|
||||||
|
Width = arg["width"]?.ToObject<long>() ?? -1
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task<JObject> GetAuthorizedPlayer(string id, HttpClient client)
|
||||||
|
{
|
||||||
|
HttpRequestMessage hrm = new(HttpMethod.Post,
|
||||||
|
"https://www.youtube.com/youtubei/v1/player?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8");
|
||||||
|
|
||||||
|
byte[] buffer = Encoding.UTF8.GetBytes(
|
||||||
|
RequestContext.BuildRequestContextJson(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["videoId"] = id
|
||||||
|
}));
|
||||||
|
ByteArrayContent byteContent = new(buffer);
|
||||||
|
byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||||
|
hrm.Content = byteContent;
|
||||||
|
|
||||||
|
if (UseAuthorization)
|
||||||
|
{
|
||||||
|
hrm.Headers.Add("Cookie", GenerateAuthCookie());
|
||||||
|
hrm.Headers.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0");
|
||||||
|
hrm.Headers.Add("Authorization", GenerateAuthHeader());
|
||||||
|
hrm.Headers.Add("X-Origin", "https://www.youtube.com");
|
||||||
|
hrm.Headers.Add("X-Youtube-Client-Name", "1");
|
||||||
|
hrm.Headers.Add("X-Youtube-Client-Version", "2.20210721.00.00");
|
||||||
|
hrm.Headers.Add("Accept-Language", "en-US;q=0.8,en;q=0.7");
|
||||||
|
hrm.Headers.Add("Origin", "https://www.youtube.com");
|
||||||
|
hrm.Headers.Add("Referer", "https://www.youtube.com/watch?v=" + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponseMessage ytPlayerRequest = await client.SendAsync(hrm);
|
||||||
|
return JObject.Parse(await ytPlayerRequest.Content.ReadAsStringAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string GenerateAuthHeader()
|
||||||
|
{
|
||||||
|
if (!UseAuthorization) return "None none";
|
||||||
|
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
string hashInput = timestamp + " " + Sapisid + " https://www.youtube.com";
|
||||||
|
string hashDigest = GenerateSha1Hash(hashInput);
|
||||||
|
return $"SAPISIDHASH {timestamp}_{hashDigest}";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string GenerateAuthCookie() => UseAuthorization ? $"SAPISID={Sapisid}; __Secure-3PAPISID={Sapisid}; __Secure-3PSID={Psid};" : ";";
|
||||||
|
|
||||||
|
private static string GenerateSha1Hash(string input)
|
||||||
|
{
|
||||||
|
using SHA1Managed sha1 = new();
|
||||||
|
byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(input));
|
||||||
|
StringBuilder sb = new(hash.Length * 2);
|
||||||
|
foreach (byte b in hash) sb.Append(b.ToString("X2"));
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetExtension(this Format format)
|
||||||
|
{
|
||||||
|
if (format.VideoCodec != "none") return "mp4";
|
||||||
|
else
|
||||||
|
switch (format.FormatId)
|
||||||
|
{
|
||||||
|
case "139":
|
||||||
|
case "140":
|
||||||
|
case "141":
|
||||||
|
case "256":
|
||||||
|
case "258":
|
||||||
|
case "327":
|
||||||
|
return "mp3";
|
||||||
|
case "249":
|
||||||
|
case "250":
|
||||||
|
case "251":
|
||||||
|
case "338":
|
||||||
|
return "opus";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "mp4";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetAuthorization(bool canUseAuthorizedEndpoints, string sapisid, string psid)
|
||||||
|
{
|
||||||
|
UseAuthorization = canUseAuthorizedEndpoints;
|
||||||
|
Sapisid = sapisid;
|
||||||
|
Psid = psid;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string GetCodec(string mimetypeString, bool audioCodec)
|
||||||
|
{
|
||||||
|
string acodec = "";
|
||||||
|
string vcodec = "";
|
||||||
|
|
||||||
|
Match match = Regex.Match(mimetypeString, "codecs=\"([\\s\\S]+?)\"");
|
||||||
|
|
||||||
|
string[] g = match.Groups[1].ToString().Split(",");
|
||||||
|
foreach (string codec in g)
|
||||||
|
{
|
||||||
|
switch (codec.Split(".")[0].Trim())
|
||||||
|
{
|
||||||
|
case "avc1":
|
||||||
|
case "av01":
|
||||||
|
case "vp9":
|
||||||
|
case "mp4v":
|
||||||
|
vcodec = codec;
|
||||||
|
break;
|
||||||
|
case "mp4a":
|
||||||
|
case "opus":
|
||||||
|
acodec = codec;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Console.WriteLine("Unknown codec type: " + codec.Split(".")[0].Trim());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (audioCodec ? acodec : vcodec).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetFormatName(JToken formatToken)
|
||||||
|
{
|
||||||
|
string format = formatToken["itag"]?.ToString() switch
|
||||||
|
{
|
||||||
|
"160" => "144p",
|
||||||
|
"278" => "144p",
|
||||||
|
"330" => "144p",
|
||||||
|
"394" => "144p",
|
||||||
|
"694" => "144p",
|
||||||
|
|
||||||
|
"133" => "240p",
|
||||||
|
"242" => "240p",
|
||||||
|
"331" => "240p",
|
||||||
|
"395" => "240p",
|
||||||
|
"695" => "240p",
|
||||||
|
|
||||||
|
"134" => "360p",
|
||||||
|
"243" => "360p",
|
||||||
|
"332" => "360p",
|
||||||
|
"396" => "360p",
|
||||||
|
"696" => "360p",
|
||||||
|
|
||||||
|
"135" => "480p",
|
||||||
|
"244" => "480p",
|
||||||
|
"333" => "480p",
|
||||||
|
"397" => "480p",
|
||||||
|
"697" => "480p",
|
||||||
|
|
||||||
|
"136" => "720p",
|
||||||
|
"247" => "720p",
|
||||||
|
"298" => "720p",
|
||||||
|
"302" => "720p",
|
||||||
|
"334" => "720p",
|
||||||
|
"398" => "720p",
|
||||||
|
"698" => "720p",
|
||||||
|
|
||||||
|
"137" => "1080p",
|
||||||
|
"299" => "1080p",
|
||||||
|
"248" => "1080p",
|
||||||
|
"303" => "1080p",
|
||||||
|
"335" => "1080p",
|
||||||
|
"399" => "1080p",
|
||||||
|
"699" => "1080p",
|
||||||
|
|
||||||
|
"264" => "1440p",
|
||||||
|
"271" => "1440p",
|
||||||
|
"304" => "1440p",
|
||||||
|
"308" => "1440p",
|
||||||
|
"336" => "1440p",
|
||||||
|
"400" => "1440p",
|
||||||
|
"700" => "1440p",
|
||||||
|
|
||||||
|
"266" => "2160p",
|
||||||
|
"305" => "2160p",
|
||||||
|
"313" => "2160p",
|
||||||
|
"315" => "2160p",
|
||||||
|
"337" => "2160p",
|
||||||
|
"401" => "2160p",
|
||||||
|
"701" => "2160p",
|
||||||
|
|
||||||
|
"138" => "4320p",
|
||||||
|
"272" => "4320p",
|
||||||
|
"402" => "4320p",
|
||||||
|
"571" => "4320p",
|
||||||
|
|
||||||
|
var _ => $"{formatToken["height"]}p",
|
||||||
|
};
|
||||||
|
|
||||||
|
return format == "p"
|
||||||
|
? formatToken["audioQuality"]?.ToString().ToLowerInvariant()
|
||||||
|
: (formatToken["fps"]?.ToObject<int>() ?? 0) > 30
|
||||||
|
? $"{format}{formatToken["fps"]}"
|
||||||
|
: format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
790
core/InnerTube/Youtube.cs
Normal file
790
core/InnerTube/Youtube.cs
Normal file
@ -0,0 +1,790 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using InnerTube.Models;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace InnerTube
|
||||||
|
{
|
||||||
|
public class Youtube
|
||||||
|
{
|
||||||
|
internal readonly HttpClient Client = new();
|
||||||
|
|
||||||
|
public readonly Dictionary<string, CacheItem<YoutubePlayer>> PlayerCache = new();
|
||||||
|
|
||||||
|
private readonly Dictionary<ChannelTabs, string> ChannelTabParams = new()
|
||||||
|
{
|
||||||
|
[ChannelTabs.Home] = @"EghmZWF0dXJlZA%3D%3D",
|
||||||
|
[ChannelTabs.Videos] = @"EgZ2aWRlb3M%3D",
|
||||||
|
[ChannelTabs.Playlists] = @"EglwbGF5bGlzdHM%3D",
|
||||||
|
[ChannelTabs.Community] = @"Egljb21tdW5pdHk%3D",
|
||||||
|
[ChannelTabs.Channels] = @"EghjaGFubmVscw%3D%3D",
|
||||||
|
[ChannelTabs.About] = @"EgVhYm91dA%3D%3D"
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task<JObject> MakeRequest(string endpoint, Dictionary<string, object> postData, string language,
|
||||||
|
string region, string clientName = "WEB", string clientId = "1", string clientVersion = "2.20220405", bool authorized = false)
|
||||||
|
{
|
||||||
|
HttpRequestMessage hrm = new(HttpMethod.Post,
|
||||||
|
@$"https://www.youtube.com/youtubei/v1/{endpoint}?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8");
|
||||||
|
|
||||||
|
byte[] buffer = Encoding.UTF8.GetBytes(RequestContext.BuildRequestContextJson(postData, language, region, clientName, clientVersion));
|
||||||
|
ByteArrayContent byteContent = new(buffer);
|
||||||
|
if (authorized)
|
||||||
|
{
|
||||||
|
hrm.Headers.Add("Cookie", Utils.GenerateAuthCookie());
|
||||||
|
hrm.Headers.Add("Authorization", Utils.GenerateAuthHeader());
|
||||||
|
hrm.Headers.Add("X-Youtube-Client-Name", clientId);
|
||||||
|
hrm.Headers.Add("X-Youtube-Client-Version", clientVersion);
|
||||||
|
hrm.Headers.Add("Origin", "https://www.youtube.com");
|
||||||
|
}
|
||||||
|
byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||||
|
hrm.Content = byteContent;
|
||||||
|
HttpResponseMessage ytPlayerRequest = await Client.SendAsync(hrm);
|
||||||
|
|
||||||
|
return JObject.Parse(await ytPlayerRequest.Content.ReadAsStringAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YoutubePlayer> GetPlayerAsync(string videoId, string language = "en", string region = "US", bool iOS = false)
|
||||||
|
{
|
||||||
|
if (PlayerCache.Any(x => x.Key == videoId && x.Value.ExpireTime > DateTimeOffset.Now))
|
||||||
|
{
|
||||||
|
CacheItem<YoutubePlayer> item = PlayerCache[videoId];
|
||||||
|
item.Item.ExpiresInSeconds = ((int)(item.ExpireTime - DateTimeOffset.Now).TotalSeconds).ToString();
|
||||||
|
return item.Item;
|
||||||
|
}
|
||||||
|
|
||||||
|
JObject player = await MakeRequest("player", new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["videoId"] = videoId,
|
||||||
|
["contentCheckOk"] = true,
|
||||||
|
["racyCheckOk"] = true
|
||||||
|
}, language, region, iOS ? "IOS" : "ANDROID", iOS ? "5" : "3", "17.13.3", true);
|
||||||
|
|
||||||
|
switch (player["playabilityStatus"]?["status"]?.ToString())
|
||||||
|
{
|
||||||
|
case "OK":
|
||||||
|
YoutubeStoryboardSpec storyboardSpec =
|
||||||
|
new(player["storyboards"]?["playerStoryboardSpecRenderer"]?["spec"]?.ToString(), player["videoDetails"]?["lengthSeconds"]?.ToObject<long>() ?? 0);
|
||||||
|
YoutubePlayer video = new()
|
||||||
|
{
|
||||||
|
Id = player["videoDetails"]?["videoId"]?.ToString(),
|
||||||
|
Title = player["videoDetails"]?["title"]?.ToString(),
|
||||||
|
Description = player["videoDetails"]?["shortDescription"]?.ToString(),
|
||||||
|
Tags = player["videoDetails"]?["keywords"]?.ToObject<string[]>(),
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = player["videoDetails"]?["author"]?.ToString(),
|
||||||
|
Id = player["videoDetails"]?["channelId"]?.ToString(),
|
||||||
|
Avatars = Array.Empty<Thumbnail>()
|
||||||
|
},
|
||||||
|
Duration = player["videoDetails"]?["lengthSeconds"]?.ToObject<long>(),
|
||||||
|
IsLive = player["videoDetails"]?["isLiveContent"]?.ToObject<bool>() ?? false,
|
||||||
|
Chapters = Array.Empty<Chapter>(),
|
||||||
|
Thumbnails = player["videoDetails"]?["thumbnail"]?["thumbnails"]?.Select(x => new Thumbnail
|
||||||
|
{
|
||||||
|
Height = x["height"]?.ToObject<int>() ?? -1,
|
||||||
|
Url = x["url"]?.ToString(),
|
||||||
|
Width = x["width"]?.ToObject<int>() ?? -1
|
||||||
|
}).ToArray(),
|
||||||
|
Formats = player["streamingData"]?["formats"]?.Select(x => new Format
|
||||||
|
{
|
||||||
|
FormatName = Utils.GetFormatName(x),
|
||||||
|
FormatId = x["itag"]?.ToString(),
|
||||||
|
FormatNote = x["quality"]?.ToString(),
|
||||||
|
Filesize = x["contentLength"]?.ToObject<long>(),
|
||||||
|
Bitrate = x["bitrate"]?.ToObject<long>() ?? 0,
|
||||||
|
AudioCodec = Utils.GetCodec(x["mimeType"]?.ToString(), true),
|
||||||
|
VideoCodec = Utils.GetCodec(x["mimeType"]?.ToString(), false),
|
||||||
|
AudioSampleRate = x["audioSampleRate"]?.ToObject<long>(),
|
||||||
|
Resolution = $"{x["width"] ?? "0"}x{x["height"] ?? "0"}",
|
||||||
|
Url = x["url"]?.ToString()
|
||||||
|
}).ToArray() ?? Array.Empty<Format>(),
|
||||||
|
AdaptiveFormats = player["streamingData"]?["adaptiveFormats"]?.Select(x => new Format
|
||||||
|
{
|
||||||
|
FormatName = Utils.GetFormatName(x),
|
||||||
|
FormatId = x["itag"]?.ToString(),
|
||||||
|
FormatNote = x["quality"]?.ToString(),
|
||||||
|
Filesize = x["contentLength"]?.ToObject<long>(),
|
||||||
|
Bitrate = x["bitrate"]?.ToObject<long>() ?? 0,
|
||||||
|
AudioCodec = Utils.GetCodec(x["mimeType"].ToString(), true),
|
||||||
|
VideoCodec = Utils.GetCodec(x["mimeType"].ToString(), false),
|
||||||
|
AudioSampleRate = x["audioSampleRate"]?.ToObject<long>(),
|
||||||
|
Resolution = $"{x["width"] ?? "0"}x{x["height"] ?? "0"}",
|
||||||
|
Url = x["url"]?.ToString(),
|
||||||
|
InitRange = x["initRange"]?.ToObject<Models.Range>(),
|
||||||
|
IndexRange = x["indexRange"]?.ToObject<Models.Range>()
|
||||||
|
}).ToArray() ?? Array.Empty<Format>(),
|
||||||
|
HlsManifestUrl = player["streamingData"]?["hlsManifestUrl"]?.ToString(),
|
||||||
|
Subtitles = player["captions"]?["playerCaptionsTracklistRenderer"]?["captionTracks"]?.Select(
|
||||||
|
x => new Subtitle
|
||||||
|
{
|
||||||
|
Ext = HttpUtility.ParseQueryString(x["baseUrl"].ToString()).Get("fmt"),
|
||||||
|
Language = Utils.ReadRuns(x["name"]?["runs"]?.ToObject<JArray>()),
|
||||||
|
Url = x["baseUrl"].ToString()
|
||||||
|
}).ToArray(),
|
||||||
|
Storyboards = storyboardSpec.Urls.TryGetValue("L0", out string sb) ? new[] { sb } : Array.Empty<string>(),
|
||||||
|
ExpiresInSeconds = player["streamingData"]?["expiresInSeconds"]?.ToString(),
|
||||||
|
ErrorMessage = null
|
||||||
|
};
|
||||||
|
PlayerCache.Remove(videoId);
|
||||||
|
PlayerCache.Add(videoId,
|
||||||
|
new CacheItem<YoutubePlayer>(video,
|
||||||
|
TimeSpan.FromSeconds(int.Parse(video.ExpiresInSeconds ?? "21600"))
|
||||||
|
.Subtract(TimeSpan.FromHours(1))));
|
||||||
|
return video;
|
||||||
|
case "LOGIN_REQUIRED":
|
||||||
|
return new YoutubePlayer
|
||||||
|
{
|
||||||
|
Id = "",
|
||||||
|
Title = "",
|
||||||
|
Description = "",
|
||||||
|
Tags = Array.Empty<string>(),
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = "",
|
||||||
|
Id = "",
|
||||||
|
SubscriberCount = "",
|
||||||
|
Avatars = Array.Empty<Thumbnail>()
|
||||||
|
},
|
||||||
|
Duration = 0,
|
||||||
|
IsLive = false,
|
||||||
|
Chapters = Array.Empty<Chapter>(),
|
||||||
|
Thumbnails = Array.Empty<Thumbnail>(),
|
||||||
|
Formats = Array.Empty<Format>(),
|
||||||
|
AdaptiveFormats = Array.Empty<Format>(),
|
||||||
|
Subtitles = Array.Empty<Subtitle>(),
|
||||||
|
Storyboards = Array.Empty<string>(),
|
||||||
|
ExpiresInSeconds = "0",
|
||||||
|
ErrorMessage =
|
||||||
|
"This video is age-restricted. Please contact this instances authors to update their configuration"
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return new YoutubePlayer
|
||||||
|
{
|
||||||
|
Id = "",
|
||||||
|
Title = "",
|
||||||
|
Description = "",
|
||||||
|
Tags = Array.Empty<string>(),
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = "",
|
||||||
|
Id = "",
|
||||||
|
SubscriberCount = "",
|
||||||
|
Avatars = Array.Empty<Thumbnail>()
|
||||||
|
},
|
||||||
|
Duration = 0,
|
||||||
|
IsLive = false,
|
||||||
|
Chapters = Array.Empty<Chapter>(),
|
||||||
|
Thumbnails = Array.Empty<Thumbnail>(),
|
||||||
|
Formats = Array.Empty<Format>(),
|
||||||
|
AdaptiveFormats = Array.Empty<Format>(),
|
||||||
|
Subtitles = Array.Empty<Subtitle>(),
|
||||||
|
Storyboards = Array.Empty<string>(),
|
||||||
|
ExpiresInSeconds = "0",
|
||||||
|
ErrorMessage = player["playabilityStatus"]?["reason"]?.ToString() ?? "Something has gone *really* wrong"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YoutubeVideo> GetVideoAsync(string videoId, string language = "en", string region = "US")
|
||||||
|
{
|
||||||
|
JObject player = await MakeRequest("next", new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["videoId"] = videoId
|
||||||
|
}, language, region);
|
||||||
|
|
||||||
|
JToken[] contents =
|
||||||
|
(player?["contents"]?["twoColumnWatchNextResults"]?["results"]?["results"]?["contents"]
|
||||||
|
?.ToObject<JArray>() ?? new JArray())
|
||||||
|
.SkipWhile(x => !x.First.Path.EndsWith("videoPrimaryInfoRenderer")).ToArray();
|
||||||
|
|
||||||
|
YoutubeVideo video = new();
|
||||||
|
video.Id = player["currentVideoEndpoint"]?["watchEndpoint"]?["videoId"]?.ToString();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
video.Title = Utils.ReadRuns(
|
||||||
|
contents[0]
|
||||||
|
["videoPrimaryInfoRenderer"]?["title"]?["runs"]?.ToObject<JArray>());
|
||||||
|
video.Description = Utils.ReadRuns(
|
||||||
|
contents[1]
|
||||||
|
["videoSecondaryInfoRenderer"]?["description"]?["runs"]?.ToObject<JArray>());
|
||||||
|
video.Views = contents[0]
|
||||||
|
["videoPrimaryInfoRenderer"]?["viewCount"]?["videoViewCountRenderer"]?["viewCount"]?["simpleText"]?.ToString();
|
||||||
|
video.Channel = new Channel
|
||||||
|
{
|
||||||
|
Name =
|
||||||
|
contents[1]
|
||||||
|
["videoSecondaryInfoRenderer"]?["owner"]?["videoOwnerRenderer"]?["title"]?["runs"]?[0]?[
|
||||||
|
"text"]?.ToString(),
|
||||||
|
Id = contents[1]
|
||||||
|
["videoSecondaryInfoRenderer"]?["owner"]?["videoOwnerRenderer"]?["title"]?["runs"]?[0]?
|
||||||
|
["navigationEndpoint"]?["browseEndpoint"]?["browseId"]?.ToString(),
|
||||||
|
SubscriberCount =
|
||||||
|
contents[1]
|
||||||
|
["videoSecondaryInfoRenderer"]?["owner"]?["videoOwnerRenderer"]?["subscriberCountText"]?[
|
||||||
|
"simpleText"]?.ToString(),
|
||||||
|
Avatars =
|
||||||
|
(contents[1][
|
||||||
|
"videoSecondaryInfoRenderer"]?["owner"]?["videoOwnerRenderer"]?["thumbnail"]?[
|
||||||
|
"thumbnails"]
|
||||||
|
?.ToObject<JArray>() ?? new JArray()).Select(Utils.ParseThumbnails).ToArray()
|
||||||
|
};
|
||||||
|
video.UploadDate = contents[0][
|
||||||
|
"videoPrimaryInfoRenderer"]?["dateText"]?["simpleText"]?.ToString();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
video.Title ??= "";
|
||||||
|
video.Description ??= "";
|
||||||
|
video.Channel ??= new Channel
|
||||||
|
{
|
||||||
|
Name = "",
|
||||||
|
Id = "",
|
||||||
|
SubscriberCount = "",
|
||||||
|
Avatars = Array.Empty<Thumbnail>()
|
||||||
|
};
|
||||||
|
video.UploadDate ??= "";
|
||||||
|
}
|
||||||
|
video.Recommended = ParseRenderers(
|
||||||
|
player?["contents"]?["twoColumnWatchNextResults"]?["secondaryResults"]?["secondaryResults"]?
|
||||||
|
["results"]?.ToObject<JArray>() ?? new JArray());
|
||||||
|
|
||||||
|
return video;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YoutubeSearchResults> SearchAsync(string query, string continuation = null,
|
||||||
|
string language = "en", string region = "US")
|
||||||
|
{
|
||||||
|
Dictionary<string, object> data = new();
|
||||||
|
if (string.IsNullOrWhiteSpace(continuation))
|
||||||
|
data.Add("query", query);
|
||||||
|
else
|
||||||
|
data.Add("continuation", continuation);
|
||||||
|
JObject search = await MakeRequest("search", data, language, region);
|
||||||
|
|
||||||
|
return new YoutubeSearchResults
|
||||||
|
{
|
||||||
|
Refinements = search?["refinements"]?.ToObject<string[]>() ?? Array.Empty<string>(),
|
||||||
|
EstimatedResults = search?["estimatedResults"]?.ToObject<long>() ?? 0,
|
||||||
|
Results = ParseRenderers(
|
||||||
|
search?["contents"]?["twoColumnSearchResultsRenderer"]?["primaryContents"]?["sectionListRenderer"]?
|
||||||
|
["contents"]?[0]?["itemSectionRenderer"]?["contents"]?.ToObject<JArray>() ??
|
||||||
|
search?["onResponseReceivedCommands"]?[0]?["appendContinuationItemsAction"]?["continuationItems"]?
|
||||||
|
[0]?["itemSectionRenderer"]?["contents"]?.ToObject<JArray>() ?? new JArray()),
|
||||||
|
ContinuationKey =
|
||||||
|
search?["contents"]?["twoColumnSearchResultsRenderer"]?["primaryContents"]?["sectionListRenderer"]?
|
||||||
|
["contents"]?[1]?["continuationItemRenderer"]?["continuationEndpoint"]?["continuationCommand"]?
|
||||||
|
["token"]?.ToString() ??
|
||||||
|
search?["onResponseReceivedCommands"]?[0]?["appendContinuationItemsAction"]?["continuationItems"]?
|
||||||
|
[1]?["continuationItemRenderer"]?["continuationEndpoint"]?["continuationCommand"]?["token"]
|
||||||
|
?.ToString() ?? ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YoutubePlaylist> GetPlaylistAsync(string id, string continuation = null,
|
||||||
|
string language = "en", string region = "US")
|
||||||
|
{
|
||||||
|
Dictionary<string, object> data = new();
|
||||||
|
if (string.IsNullOrWhiteSpace(continuation))
|
||||||
|
data.Add("browseId", "VL" + id);
|
||||||
|
else
|
||||||
|
data.Add("continuation", continuation);
|
||||||
|
JObject playlist = await MakeRequest("browse", data, language, region);
|
||||||
|
DynamicItem[] renderers = ParseRenderers(
|
||||||
|
playlist?["contents"]?["twoColumnBrowseResultsRenderer"]?["tabs"]?[0]?["tabRenderer"]?["content"]?
|
||||||
|
["sectionListRenderer"]?["contents"]?[0]?["itemSectionRenderer"]?["contents"]?[0]?
|
||||||
|
["playlistVideoListRenderer"]?["contents"]?.ToObject<JArray>() ??
|
||||||
|
playlist?["onResponseReceivedActions"]?[0]?["appendContinuationItemsAction"]?["continuationItems"]
|
||||||
|
?.ToObject<JArray>() ?? new JArray());
|
||||||
|
|
||||||
|
return new YoutubePlaylist
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Title = playlist?["metadata"]?["playlistMetadataRenderer"]?["title"]?.ToString(),
|
||||||
|
Description = playlist?["metadata"]?["playlistMetadataRenderer"]?["description"]?.ToString(),
|
||||||
|
VideoCount = playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[0]?[
|
||||||
|
"playlistSidebarPrimaryInfoRenderer"]?["stats"]?[0]?["runs"]?[0]?["text"]?.ToString(),
|
||||||
|
ViewCount = playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[0]?[
|
||||||
|
"playlistSidebarPrimaryInfoRenderer"]?["stats"]?[1]?["simpleText"]?.ToString(),
|
||||||
|
LastUpdated = Utils.ReadRuns(playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[0]?[
|
||||||
|
"playlistSidebarPrimaryInfoRenderer"]?["stats"]?[2]?["runs"]?.ToObject<JArray>() ?? new JArray()),
|
||||||
|
Thumbnail = (playlist?["microformat"]?["microformatDataRenderer"]?["thumbnail"]?["thumbnails"] ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name =
|
||||||
|
playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[1]?
|
||||||
|
["playlistSidebarSecondaryInfoRenderer"]?["videoOwner"]?["videoOwnerRenderer"]?["title"]?
|
||||||
|
["runs"]?[0]?["text"]?.ToString(),
|
||||||
|
Id = playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[1]?
|
||||||
|
["playlistSidebarSecondaryInfoRenderer"]?["videoOwner"]?["videoOwnerRenderer"]?
|
||||||
|
["navigationEndpoint"]?["browseEndpoint"]?["browseId"]?.ToString(),
|
||||||
|
SubscriberCount = "",
|
||||||
|
Avatars =
|
||||||
|
(playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[1]?
|
||||||
|
["playlistSidebarSecondaryInfoRenderer"]?["videoOwner"]?["videoOwnerRenderer"]?["thumbnail"]
|
||||||
|
?["thumbnails"] ?? new JArray()).Select(Utils.ParseThumbnails).ToArray()
|
||||||
|
},
|
||||||
|
Videos = renderers.Where(x => x is not ContinuationItem).ToArray(),
|
||||||
|
ContinuationKey = renderers.FirstOrDefault(x => x is ContinuationItem)?.Id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YoutubeChannel> GetChannelAsync(string id, ChannelTabs tab = ChannelTabs.Home,
|
||||||
|
string continuation = null, string language = "en", string region = "US")
|
||||||
|
{
|
||||||
|
Dictionary<string, object> data = new();
|
||||||
|
if (string.IsNullOrWhiteSpace(continuation))
|
||||||
|
{
|
||||||
|
data.Add("browseId", id);
|
||||||
|
if (string.IsNullOrWhiteSpace(continuation))
|
||||||
|
data.Add("params", ChannelTabParams[tab]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data.Add("continuation", continuation);
|
||||||
|
}
|
||||||
|
|
||||||
|
JObject channel = await MakeRequest("browse", data, language, region);
|
||||||
|
JArray mainArray =
|
||||||
|
(channel?["contents"]?["twoColumnBrowseResultsRenderer"]?["tabs"]?.ToObject<JArray>() ?? new JArray())
|
||||||
|
.FirstOrDefault(x => x?["tabRenderer"]?["selected"]?.ToObject<bool>() ?? false)?["tabRenderer"]?[
|
||||||
|
"content"]?
|
||||||
|
["sectionListRenderer"]?["contents"]?.ToObject<JArray>();
|
||||||
|
|
||||||
|
return new YoutubeChannel
|
||||||
|
{
|
||||||
|
Id = channel?["metadata"]?["channelMetadataRenderer"]?["externalId"]?.ToString(),
|
||||||
|
Name = channel?["metadata"]?["channelMetadataRenderer"]?["title"]?.ToString(),
|
||||||
|
Url = channel?["metadata"]?["channelMetadataRenderer"]?["externalId"]?.ToString(),
|
||||||
|
Avatars = (channel?["metadata"]?["channelMetadataRenderer"]?["avatar"]?["thumbnails"] ?? new JArray())
|
||||||
|
.Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
Banners = (channel?["header"]?["c4TabbedHeaderRenderer"]?["banner"]?["thumbnails"] ?? new JArray())
|
||||||
|
.Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
Description = channel?["metadata"]?["channelMetadataRenderer"]?["description"]?.ToString(),
|
||||||
|
Videos = ParseRenderers(mainArray ??
|
||||||
|
channel?["onResponseReceivedActions"]?[0]?["appendContinuationItemsAction"]?
|
||||||
|
["continuationItems"]?.ToObject<JArray>() ?? new JArray()),
|
||||||
|
Subscribers = channel?["header"]?["c4TabbedHeaderRenderer"]?["subscriberCountText"]?["simpleText"]
|
||||||
|
?.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YoutubeTrends> GetExploreAsync(string browseId = null, string continuation = null, string language = "en", string region = "US")
|
||||||
|
{
|
||||||
|
Dictionary<string, object> data = new();
|
||||||
|
if (string.IsNullOrWhiteSpace(continuation))
|
||||||
|
{
|
||||||
|
data.Add("browseId", browseId ?? "FEexplore");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data.Add("continuation", continuation);
|
||||||
|
}
|
||||||
|
|
||||||
|
JObject explore = await MakeRequest("browse", data, language, region);
|
||||||
|
JToken[] token =
|
||||||
|
(explore?["contents"]?["twoColumnBrowseResultsRenderer"]?["tabs"]?[0]?["tabRenderer"]?["content"]?
|
||||||
|
["sectionListRenderer"]?["contents"]?.ToObject<JArray>() ?? new JArray()).Skip(1).ToArray();
|
||||||
|
|
||||||
|
JArray mainArray = new(token.Select(x => x is JObject obj ? obj : null).Where(x => x is not null));
|
||||||
|
|
||||||
|
return new YoutubeTrends
|
||||||
|
{
|
||||||
|
Categories = explore?["contents"]?["twoColumnBrowseResultsRenderer"]?["tabs"]?[0]?["tabRenderer"]?["content"]?["sectionListRenderer"]?["contents"]?[0]?["itemSectionRenderer"]?["contents"]?[0]?["destinationShelfRenderer"]?["destinationButtons"]?.Select(
|
||||||
|
x =>
|
||||||
|
{
|
||||||
|
JToken rendererObject = x?["destinationButtonRenderer"];
|
||||||
|
TrendCategory category = new()
|
||||||
|
{
|
||||||
|
Label = rendererObject?["label"]?["simpleText"]?.ToString(),
|
||||||
|
BackgroundImage = (rendererObject?["backgroundImage"]?["thumbnails"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
Icon = (rendererObject?["iconImage"]?["thumbnails"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
Id = $"{rendererObject?["onTap"]?["browseEndpoint"]?["browseId"]}"
|
||||||
|
};
|
||||||
|
return category;
|
||||||
|
}).ToArray(),
|
||||||
|
Videos = ParseRenderers(mainArray)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YoutubeLocals> GetLocalsAsync(string language = "en", string region = "US")
|
||||||
|
{
|
||||||
|
JObject locals = await MakeRequest("account/account_menu", new Dictionary<string, object>(), language,
|
||||||
|
region);
|
||||||
|
|
||||||
|
return new YoutubeLocals
|
||||||
|
{
|
||||||
|
Languages =
|
||||||
|
locals["actions"]?[0]?["openPopupAction"]?["popup"]?["multiPageMenuRenderer"]?["sections"]?[0]?
|
||||||
|
["multiPageMenuSectionRenderer"]?["items"]?[1]?["compactLinkRenderer"]?["serviceEndpoint"]?
|
||||||
|
["signalServiceEndpoint"]?["actions"]?[0]?["getMultiPageMenuAction"]?["menu"]?
|
||||||
|
["multiPageMenuRenderer"]?["sections"]?[0]?["multiPageMenuSectionRenderer"]?["items"]?
|
||||||
|
.ToObject<JArray>()?.ToDictionary(
|
||||||
|
x => x?["compactLinkRenderer"]?["serviceEndpoint"]?["signalServiceEndpoint"]?
|
||||||
|
["actions"]?[0]?["selectLanguageCommand"]?["hl"]?.ToString(),
|
||||||
|
x => x?["compactLinkRenderer"]?["title"]?["simpleText"]?.ToString()),
|
||||||
|
Regions =
|
||||||
|
locals["actions"]?[0]?["openPopupAction"]?["popup"]?["multiPageMenuRenderer"]?["sections"]?[0]?
|
||||||
|
["multiPageMenuSectionRenderer"]?["items"]?[2]?["compactLinkRenderer"]?["serviceEndpoint"]?
|
||||||
|
["signalServiceEndpoint"]?["actions"]?[0]?["getMultiPageMenuAction"]?["menu"]?
|
||||||
|
["multiPageMenuRenderer"]?["sections"]?[0]?["multiPageMenuSectionRenderer"]?["items"]?
|
||||||
|
.ToObject<JArray>()?.ToDictionary(
|
||||||
|
x => x?["compactLinkRenderer"]?["serviceEndpoint"]?["signalServiceEndpoint"]?
|
||||||
|
["actions"]?[0]?["selectCountryCommand"]?["gl"]?.ToString(),
|
||||||
|
x => x?["compactLinkRenderer"]?["title"]?["simpleText"]?.ToString())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private DynamicItem[] ParseRenderers(JArray renderersArray)
|
||||||
|
{
|
||||||
|
List<DynamicItem> items = new();
|
||||||
|
|
||||||
|
foreach (JToken jToken in renderersArray)
|
||||||
|
{
|
||||||
|
JObject recommendationContainer = jToken as JObject;
|
||||||
|
string rendererName = recommendationContainer?.First?.Path.Split(".").Last() ?? "";
|
||||||
|
JObject rendererItem = recommendationContainer?[rendererName]?.ToObject<JObject>();
|
||||||
|
switch (rendererName)
|
||||||
|
{
|
||||||
|
case "videoRenderer":
|
||||||
|
items.Add(new VideoItem
|
||||||
|
{
|
||||||
|
Id = rendererItem?["videoId"]?.ToString(),
|
||||||
|
Title = Utils.ReadRuns(rendererItem?["title"]?["runs"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()),
|
||||||
|
Thumbnails =
|
||||||
|
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
UploadedAt = rendererItem?["publishedTimeText"]?["simpleText"]?.ToString(),
|
||||||
|
Views = long.TryParse(
|
||||||
|
rendererItem?["viewCountText"]?["simpleText"]?.ToString().Split(" ")[0]
|
||||||
|
.Replace(",", "").Replace(".", "") ?? "0", out long vV) ? vV : 0,
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = rendererItem?["longBylineText"]?["runs"]?[0]?["text"]?.ToString(),
|
||||||
|
Id = rendererItem?["longBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
|
||||||
|
"browseEndpoint"]?["browseId"]?.ToString(),
|
||||||
|
SubscriberCount = null,
|
||||||
|
Avatars =
|
||||||
|
(rendererItem?["channelThumbnailSupportedRenderers"]?[
|
||||||
|
"channelThumbnailWithLinkRenderer"]?["thumbnail"]?["thumbnails"]
|
||||||
|
?.ToObject<JArray>() ?? new JArray()).Select(Utils.ParseThumbnails)
|
||||||
|
.ToArray()
|
||||||
|
},
|
||||||
|
Duration = rendererItem?["thumbnailOverlays"]?[0]?[
|
||||||
|
"thumbnailOverlayTimeStatusRenderer"]?["text"]?["simpleText"]?.ToString(),
|
||||||
|
Description = Utils.ReadRuns(rendererItem?["detailedMetadataSnippets"]?[0]?[
|
||||||
|
"snippetText"]?["runs"]?.ToObject<JArray>() ?? new JArray())
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "gridVideoRenderer":
|
||||||
|
items.Add(new VideoItem
|
||||||
|
{
|
||||||
|
Id = rendererItem?["videoId"]?.ToString(),
|
||||||
|
Title = rendererItem?["title"]?["simpleText"]?.ToString() ?? Utils.ReadRuns(
|
||||||
|
rendererItem?["title"]?["runs"]?.ToObject<JArray>() ?? new JArray()),
|
||||||
|
Thumbnails =
|
||||||
|
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
UploadedAt = rendererItem?["publishedTimeText"]?["simpleText"]?.ToString(),
|
||||||
|
Views = long.TryParse(
|
||||||
|
rendererItem?["viewCountText"]?["simpleText"]?.ToString().Split(" ")[0]
|
||||||
|
.Replace(",", "").Replace(".", "") ?? "0", out long gVV) ? gVV : 0,
|
||||||
|
Channel = null,
|
||||||
|
Duration = rendererItem?["thumbnailOverlays"]?[0]?[
|
||||||
|
"thumbnailOverlayTimeStatusRenderer"]?["text"]?["simpleText"]?.ToString()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "playlistRenderer":
|
||||||
|
items.Add(new PlaylistItem
|
||||||
|
{
|
||||||
|
Id = rendererItem?["playlistId"]
|
||||||
|
?.ToString(),
|
||||||
|
Title = rendererItem?["title"]?["simpleText"]
|
||||||
|
?.ToString(),
|
||||||
|
Thumbnails =
|
||||||
|
(rendererItem?["thumbnails"]?[0]?["thumbnails"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
VideoCount = int.TryParse(
|
||||||
|
rendererItem?["videoCountText"]?["runs"]?[0]?["text"]?.ToString().Replace(",", "")
|
||||||
|
.Replace(".", "") ?? "0", out int pVC) ? pVC : 0,
|
||||||
|
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]
|
||||||
|
?.ToString(),
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = rendererItem?["longBylineText"]?["runs"]?[0]?["text"]
|
||||||
|
?.ToString(),
|
||||||
|
Id = rendererItem?["longBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
|
||||||
|
"browseEndpoint"]?["browseId"]
|
||||||
|
?.ToString(),
|
||||||
|
SubscriberCount = null,
|
||||||
|
Avatars = null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "channelRenderer":
|
||||||
|
items.Add(new ChannelItem
|
||||||
|
{
|
||||||
|
Id = rendererItem?["channelId"]?.ToString(),
|
||||||
|
Title = rendererItem?["title"]?["simpleText"]?.ToString(),
|
||||||
|
Thumbnails =
|
||||||
|
(rendererItem?["thumbnail"]?["thumbnails"]
|
||||||
|
?.ToObject<JArray>() ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails)
|
||||||
|
.ToArray(), //
|
||||||
|
Url = rendererItem?["navigationEndpoint"]?["commandMetadata"]?["webCommandMetadata"]?["url"]
|
||||||
|
?.ToString(),
|
||||||
|
Description =
|
||||||
|
Utils.ReadRuns(rendererItem?["descriptionSnippet"]?["runs"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()),
|
||||||
|
VideoCount = long.TryParse(
|
||||||
|
rendererItem?["videoCountText"]?["runs"]?[0]?["text"]
|
||||||
|
?.ToString()
|
||||||
|
.Replace(",",
|
||||||
|
"")
|
||||||
|
.Replace(".",
|
||||||
|
"") ??
|
||||||
|
"0", out long cVC) ? cVC : 0,
|
||||||
|
Subscribers = rendererItem?["subscriberCountText"]?["simpleText"]?.ToString()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "radioRenderer":
|
||||||
|
items.Add(new RadioItem
|
||||||
|
{
|
||||||
|
Id = rendererItem?["playlistId"]
|
||||||
|
?.ToString(),
|
||||||
|
Title = rendererItem?["title"]?["simpleText"]
|
||||||
|
?.ToString(),
|
||||||
|
Thumbnails =
|
||||||
|
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]
|
||||||
|
?.ToString(),
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = rendererItem?["longBylineText"]?["simpleText"]?.ToString(),
|
||||||
|
Id = "",
|
||||||
|
SubscriberCount = null,
|
||||||
|
Avatars = null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "shelfRenderer":
|
||||||
|
items.Add(new ShelfItem
|
||||||
|
{
|
||||||
|
Title = rendererItem?["title"]?["simpleText"]
|
||||||
|
?.ToString() ??
|
||||||
|
rendererItem?["title"]?["runs"]?[0]?["text"]
|
||||||
|
?.ToString(),
|
||||||
|
Thumbnails = (rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
Items = ParseRenderers(
|
||||||
|
rendererItem?["content"]?["verticalListRenderer"]?["items"]
|
||||||
|
?.ToObject<JArray>() ??
|
||||||
|
rendererItem?["content"]?["horizontalListRenderer"]?["items"]
|
||||||
|
?.ToObject<JArray>() ??
|
||||||
|
rendererItem?["content"]?["expandedShelfContentsRenderer"]?["items"]
|
||||||
|
?.ToObject<JArray>() ??
|
||||||
|
new JArray()),
|
||||||
|
CollapsedItemCount =
|
||||||
|
rendererItem?["content"]?["verticalListRenderer"]?["collapsedItemCount"]
|
||||||
|
?.ToObject<int>() ?? 0,
|
||||||
|
Badges = ParseRenderers(rendererItem?["badges"]?.ToObject<JArray>() ?? new JArray())
|
||||||
|
.Where(x => x is BadgeItem).Cast<BadgeItem>().ToArray(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "horizontalCardListRenderer":
|
||||||
|
items.Add(new HorizontalCardListItem
|
||||||
|
{
|
||||||
|
Title = rendererItem?["header"]?["richListHeaderRenderer"]?["title"]?["simpleText"]
|
||||||
|
?.ToString(),
|
||||||
|
Items = ParseRenderers(rendererItem?["cards"]?.ToObject<JArray>() ?? new JArray())
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "searchRefinementCardRenderer":
|
||||||
|
items.Add(new CardItem
|
||||||
|
{
|
||||||
|
Title = Utils.ReadRuns(rendererItem?["query"]?["runs"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()),
|
||||||
|
Thumbnails = (rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails).ToArray()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "compactVideoRenderer":
|
||||||
|
items.Add(new VideoItem
|
||||||
|
{
|
||||||
|
Id = rendererItem?["videoId"]?.ToString(),
|
||||||
|
Title = rendererItem?["title"]?["simpleText"]?.ToString(),
|
||||||
|
Thumbnails =
|
||||||
|
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
UploadedAt = rendererItem?["publishedTimeText"]?["simpleText"]?.ToString(),
|
||||||
|
Views = long.TryParse(
|
||||||
|
rendererItem?["viewCountText"]?["simpleText"]?.ToString().Split(" ")[0]
|
||||||
|
.Replace(",", "").Replace(".", "") ?? "0", out long cVV) ? cVV : 0,
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = rendererItem?["longBylineText"]?["runs"]?[0]?["text"]?.ToString(),
|
||||||
|
Id = rendererItem?["longBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
|
||||||
|
"browseEndpoint"]?["browseId"]?.ToString(),
|
||||||
|
SubscriberCount = null,
|
||||||
|
Avatars = null
|
||||||
|
},
|
||||||
|
Duration = rendererItem?["thumbnailOverlays"]?[0]?[
|
||||||
|
"thumbnailOverlayTimeStatusRenderer"]?["text"]?["simpleText"]?.ToString()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "compactPlaylistRenderer":
|
||||||
|
items.Add(new PlaylistItem
|
||||||
|
{
|
||||||
|
Id = rendererItem?["playlistId"]
|
||||||
|
?.ToString(),
|
||||||
|
Title = rendererItem?["title"]?["simpleText"]
|
||||||
|
?.ToString(),
|
||||||
|
Thumbnails =
|
||||||
|
(rendererItem?["thumbnail"]?["thumbnails"]
|
||||||
|
?.ToObject<JArray>() ?? new JArray()).Select(Utils.ParseThumbnails)
|
||||||
|
.ToArray(),
|
||||||
|
VideoCount = int.TryParse(
|
||||||
|
rendererItem?["videoCountText"]?["runs"]?[0]?["text"]?.ToString().Replace(",", "")
|
||||||
|
.Replace(".", "") ?? "0", out int cPVC) ? cPVC : 0,
|
||||||
|
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]
|
||||||
|
?.ToString(),
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = rendererItem?["longBylineText"]?["runs"]?[0]?["text"]
|
||||||
|
?.ToString(),
|
||||||
|
Id = rendererItem?["longBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
|
||||||
|
"browseEndpoint"]?["browseId"]
|
||||||
|
?.ToString(),
|
||||||
|
SubscriberCount = null,
|
||||||
|
Avatars = null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "compactRadioRenderer":
|
||||||
|
items.Add(new RadioItem
|
||||||
|
{
|
||||||
|
Id = rendererItem?["playlistId"]
|
||||||
|
?.ToString(),
|
||||||
|
Title = rendererItem?["title"]?["simpleText"]
|
||||||
|
?.ToString(),
|
||||||
|
Thumbnails =
|
||||||
|
(rendererItem?["thumbnail"]?["thumbnails"]
|
||||||
|
?.ToObject<JArray>() ?? new JArray()).Select(Utils.ParseThumbnails)
|
||||||
|
.ToArray(),
|
||||||
|
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]
|
||||||
|
?.ToString(),
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = rendererItem?["longBylineText"]?["simpleText"]?.ToString(),
|
||||||
|
Id = "",
|
||||||
|
SubscriberCount = null,
|
||||||
|
Avatars = null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "continuationItemRenderer":
|
||||||
|
items.Add(new ContinuationItem
|
||||||
|
{
|
||||||
|
Id = rendererItem?["continuationEndpoint"]?["continuationCommand"]?["token"]?.ToString()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "playlistVideoRenderer":
|
||||||
|
items.Add(new PlaylistVideoItem
|
||||||
|
{
|
||||||
|
Id = rendererItem?["videoId"]?.ToString(),
|
||||||
|
Index = rendererItem?["index"]?["simpleText"]?.ToObject<long>() ?? 0,
|
||||||
|
Title = Utils.ReadRuns(rendererItem?["title"]?["runs"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()),
|
||||||
|
Thumbnails =
|
||||||
|
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = rendererItem?["shortBylineText"]?["runs"]?[0]?["text"]?.ToString(),
|
||||||
|
Id = rendererItem?["shortBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
|
||||||
|
"browseEndpoint"]?["browseId"]?.ToString(),
|
||||||
|
SubscriberCount = null,
|
||||||
|
Avatars = null
|
||||||
|
},
|
||||||
|
Duration = rendererItem?["lengthText"]?["simpleText"]?.ToString()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "itemSectionRenderer":
|
||||||
|
items.Add(new ItemSectionItem
|
||||||
|
{
|
||||||
|
Contents = ParseRenderers(rendererItem?["contents"]?.ToObject<JArray>() ?? new JArray())
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "gridRenderer":
|
||||||
|
items.Add(new ItemSectionItem
|
||||||
|
{
|
||||||
|
Contents = ParseRenderers(rendererItem?["items"]?.ToObject<JArray>() ?? new JArray())
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "messageRenderer":
|
||||||
|
items.Add(new MessageItem
|
||||||
|
{
|
||||||
|
Title = rendererItem?["text"]?["simpleText"]?.ToString()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "channelAboutFullMetadataRenderer":
|
||||||
|
items.Add(new ChannelAboutItem
|
||||||
|
{
|
||||||
|
Description = rendererItem?["description"]?["simpleText"]?.ToString(),
|
||||||
|
Country = rendererItem?["country"]?["simpleText"]?.ToString(),
|
||||||
|
Joined = Utils.ReadRuns(rendererItem?["joinedDateText"]?["runs"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()),
|
||||||
|
ViewCount = rendererItem?["viewCountText"]?["simpleText"]?.ToString()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "compactStationRenderer":
|
||||||
|
items.Add(new StationItem
|
||||||
|
{
|
||||||
|
Id = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["playlistId"]?.ToString(),
|
||||||
|
Title = rendererItem?["title"]?["simpleText"]?.ToString(),
|
||||||
|
Thumbnails =
|
||||||
|
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
|
||||||
|
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
|
||||||
|
VideoCount = rendererItem?["videoCountText"]?["runs"]?[0]?["text"].ToObject<int>() ?? 0,
|
||||||
|
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]?.ToString(),
|
||||||
|
Description = rendererItem?["description"]?["simpleText"]?.ToString()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "metadataBadgeRenderer":
|
||||||
|
items.Add(new BadgeItem
|
||||||
|
{
|
||||||
|
Title = rendererItem?["label"]?.ToString(),
|
||||||
|
Style = rendererItem?["style"]?.ToString()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "promotedSparklesWebRenderer":
|
||||||
|
// this is an ad
|
||||||
|
// no one likes ads
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
items.Add(new DynamicItem
|
||||||
|
{
|
||||||
|
Id = rendererName,
|
||||||
|
Title = rendererItem?.ToString()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
core/LightTube/Contexts/BaseContext.cs
Normal file
7
core/LightTube/Contexts/BaseContext.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace LightTube.Contexts
|
||||||
|
{
|
||||||
|
public class BaseContext
|
||||||
|
{
|
||||||
|
public bool MobileLayout;
|
||||||
|
}
|
||||||
|
}
|
7
core/LightTube/Contexts/ErrorContext.cs
Normal file
7
core/LightTube/Contexts/ErrorContext.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace LightTube.Contexts
|
||||||
|
{
|
||||||
|
public class ErrorContext : BaseContext
|
||||||
|
{
|
||||||
|
public string Path;
|
||||||
|
}
|
||||||
|
}
|
11
core/LightTube/Contexts/FeedContext.cs
Normal file
11
core/LightTube/Contexts/FeedContext.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using LightTube.Database;
|
||||||
|
|
||||||
|
namespace LightTube.Contexts
|
||||||
|
{
|
||||||
|
public class FeedContext : BaseContext
|
||||||
|
{
|
||||||
|
public LTChannel[] Channels;
|
||||||
|
public FeedVideo[] Videos;
|
||||||
|
public string RssToken;
|
||||||
|
}
|
||||||
|
}
|
13
core/LightTube/Contexts/LocalsContext.cs
Normal file
13
core/LightTube/Contexts/LocalsContext.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using InnerTube.Models;
|
||||||
|
|
||||||
|
namespace LightTube.Contexts
|
||||||
|
{
|
||||||
|
public class LocalsContext : BaseContext
|
||||||
|
{
|
||||||
|
public Dictionary<string, string> Languages;
|
||||||
|
public Dictionary<string, string> Regions;
|
||||||
|
public string CurrentLanguage;
|
||||||
|
public string CurrentRegion;
|
||||||
|
}
|
||||||
|
}
|
11
core/LightTube/Contexts/PlaylistsContext.cs
Normal file
11
core/LightTube/Contexts/PlaylistsContext.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using InnerTube.Models;
|
||||||
|
using LightTube.Database;
|
||||||
|
|
||||||
|
namespace LightTube.Contexts
|
||||||
|
{
|
||||||
|
public class PlaylistsContext : BaseContext
|
||||||
|
{
|
||||||
|
public IEnumerable<LTPlaylist> Playlists;
|
||||||
|
}
|
||||||
|
}
|
351
core/LightTube/Controllers/AccountController.cs
Normal file
351
core/LightTube/Controllers/AccountController.cs
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using InnerTube;
|
||||||
|
using InnerTube.Models;
|
||||||
|
using LightTube.Contexts;
|
||||||
|
using LightTube.Database;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace LightTube.Controllers
|
||||||
|
{
|
||||||
|
public class AccountController : Controller
|
||||||
|
{
|
||||||
|
private readonly Youtube _youtube;
|
||||||
|
|
||||||
|
public AccountController(Youtube youtube)
|
||||||
|
{
|
||||||
|
_youtube = youtube;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("/Account")]
|
||||||
|
public IActionResult Account()
|
||||||
|
{
|
||||||
|
return View(new BaseContext
|
||||||
|
{
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult Login(string err = null)
|
||||||
|
{
|
||||||
|
if (HttpContext.TryGetUser(out LTUser _, "web"))
|
||||||
|
return Redirect("/");
|
||||||
|
|
||||||
|
return View(new MessageContext
|
||||||
|
{
|
||||||
|
Message = err,
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Login(string userid, string password)
|
||||||
|
{
|
||||||
|
if (HttpContext.TryGetUser(out LTUser _, "web"))
|
||||||
|
return Redirect("/");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LTLogin login = await DatabaseManager.Logins.CreateToken(userid, password, Request.Headers["user-agent"], new []{"web"});
|
||||||
|
Response.Cookies.Append("token", login.Token, new CookieOptions
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.MaxValue
|
||||||
|
});
|
||||||
|
|
||||||
|
return Redirect("/");
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException e)
|
||||||
|
{
|
||||||
|
return Redirect("/Account/Login?err=" + HttpUtility.UrlEncode(e.Message));
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException e)
|
||||||
|
{
|
||||||
|
return Redirect("/Account/Login?err=" + HttpUtility.UrlEncode(e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> Logout()
|
||||||
|
{
|
||||||
|
if (HttpContext.Request.Cookies.TryGetValue("token", out string token))
|
||||||
|
{
|
||||||
|
await DatabaseManager.Logins.RemoveToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpContext.Response.Cookies.Delete("token");
|
||||||
|
HttpContext.Response.Cookies.Delete("account_data");
|
||||||
|
return Redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult Register(string err = null)
|
||||||
|
{
|
||||||
|
if (HttpContext.TryGetUser(out LTUser _, "web"))
|
||||||
|
return Redirect("/");
|
||||||
|
|
||||||
|
return View(new MessageContext
|
||||||
|
{
|
||||||
|
Message = err,
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Register(string userid, string password)
|
||||||
|
{
|
||||||
|
if (HttpContext.TryGetUser(out LTUser _, "web"))
|
||||||
|
return Redirect("/");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DatabaseManager.Logins.CreateUser(userid, password);
|
||||||
|
LTLogin login = await DatabaseManager.Logins.CreateToken(userid, password, Request.Headers["user-agent"], new []{"web"});
|
||||||
|
Response.Cookies.Append("token", login.Token, new CookieOptions
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.MaxValue
|
||||||
|
});
|
||||||
|
|
||||||
|
return Redirect("/");
|
||||||
|
}
|
||||||
|
catch (DuplicateNameException e)
|
||||||
|
{
|
||||||
|
return Redirect("/Account/Register?err=" + HttpUtility.UrlEncode(e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult RegisterLocal()
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser _, "web"))
|
||||||
|
HttpContext.CreateLocalAccount();
|
||||||
|
|
||||||
|
return Redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult Delete(string err = null)
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser _, "web"))
|
||||||
|
return Redirect("/");
|
||||||
|
|
||||||
|
return View(new MessageContext
|
||||||
|
{
|
||||||
|
Message = err,
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Delete(string userid, string password)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (userid == "Local Account" && password == "local_account")
|
||||||
|
Response.Cookies.Delete("account_data");
|
||||||
|
else
|
||||||
|
await DatabaseManager.Logins.DeleteUser(userid, password);
|
||||||
|
return Redirect("/Account/Register?err=Account+deleted");
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException e)
|
||||||
|
{
|
||||||
|
return Redirect("/Account/Delete?err=" + HttpUtility.UrlEncode(e.Message));
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException e)
|
||||||
|
{
|
||||||
|
return Redirect("/Account/Delete?err=" + HttpUtility.UrlEncode(e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> Logins()
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser _, "web") || !HttpContext.Request.Cookies.TryGetValue("token", out string token))
|
||||||
|
return Redirect("/Account/Login");
|
||||||
|
|
||||||
|
return View(new LoginsContext
|
||||||
|
{
|
||||||
|
CurrentLogin = await DatabaseManager.Logins.GetCurrentLoginId(token),
|
||||||
|
Logins = await DatabaseManager.Logins.GetAllUserTokens(token),
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> DisableLogin(string id)
|
||||||
|
{
|
||||||
|
if (!HttpContext.Request.Cookies.TryGetValue("token", out string token))
|
||||||
|
return Redirect("/Account/Login");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DatabaseManager.Logins.RemoveTokenFromId(token, id);
|
||||||
|
} catch { }
|
||||||
|
return Redirect("/Account/Logins");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> Subscribe(string channel)
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
YoutubeChannel youtubeChannel = await _youtube.GetChannelAsync(channel, ChannelTabs.About);
|
||||||
|
|
||||||
|
(LTChannel channel, bool subscribed) result;
|
||||||
|
result.channel = await DatabaseManager.Channels.UpdateChannel(youtubeChannel.Id, youtubeChannel.Name, youtubeChannel.Subscribers,
|
||||||
|
youtubeChannel.Avatars.First().Url);
|
||||||
|
|
||||||
|
if (user.PasswordHash == "local_account")
|
||||||
|
{
|
||||||
|
LTChannel ltChannel = await DatabaseManager.Channels.UpdateChannel(youtubeChannel.Id, youtubeChannel.Name, youtubeChannel.Subscribers,
|
||||||
|
youtubeChannel.Avatars.First().Url);
|
||||||
|
if (user.SubscribedChannels.Contains(ltChannel.ChannelId))
|
||||||
|
user.SubscribedChannels.Remove(ltChannel.ChannelId);
|
||||||
|
else
|
||||||
|
user.SubscribedChannels.Add(ltChannel.ChannelId);
|
||||||
|
|
||||||
|
HttpContext.Response.Cookies.Append("account_data", JsonConvert.SerializeObject(user),
|
||||||
|
new CookieOptions
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.MaxValue
|
||||||
|
});
|
||||||
|
|
||||||
|
result.subscribed = user.SubscribedChannels.Contains(ltChannel.ChannelId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result =
|
||||||
|
await DatabaseManager.Logins.SubscribeToChannel(user, youtubeChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(result.subscribed ? "true" : "false");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult SubscriptionsJson()
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||||
|
return Json(Array.Empty<string>());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Json(user.SubscribedChannels);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Json(Array.Empty<string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> Settings()
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||||
|
Redirect("/Account/Login");
|
||||||
|
|
||||||
|
if (Request.Method == "POST")
|
||||||
|
{
|
||||||
|
CookieOptions opts = new()
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.MaxValue
|
||||||
|
};
|
||||||
|
foreach ((string key, StringValues value) in Request.Form)
|
||||||
|
{
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case "theme":
|
||||||
|
Response.Cookies.Append("theme", value, opts);
|
||||||
|
break;
|
||||||
|
case "hl":
|
||||||
|
Response.Cookies.Append("hl", value, opts);
|
||||||
|
break;
|
||||||
|
case "gl":
|
||||||
|
Response.Cookies.Append("gl", value, opts);
|
||||||
|
break;
|
||||||
|
case "compatibility":
|
||||||
|
Response.Cookies.Append("compatibility", value, opts);
|
||||||
|
break;
|
||||||
|
case "api-access":
|
||||||
|
await DatabaseManager.Logins.SetApiAccess(user, bool.Parse(value));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Redirect("/Account");
|
||||||
|
}
|
||||||
|
|
||||||
|
YoutubeLocals locals = await _youtube.GetLocalsAsync();
|
||||||
|
|
||||||
|
Request.Cookies.TryGetValue("theme", out string theme);
|
||||||
|
|
||||||
|
bool compatibility = false;
|
||||||
|
if (Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||||
|
bool.TryParse(compatibilityString, out compatibility);
|
||||||
|
|
||||||
|
return View(new SettingsContext
|
||||||
|
{
|
||||||
|
Languages = locals.Languages,
|
||||||
|
Regions = locals.Regions,
|
||||||
|
CurrentLanguage = HttpContext.GetLanguage(),
|
||||||
|
CurrentRegion = HttpContext.GetRegion(),
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request),
|
||||||
|
Theme = theme ?? "light",
|
||||||
|
CompatibilityMode = compatibility,
|
||||||
|
ApiAccess = user.ApiAccess
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> AddVideoToPlaylist(string v)
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||||
|
Redirect("/Account/Login");
|
||||||
|
|
||||||
|
JObject ytPlayer = await InnerTube.Utils.GetAuthorizedPlayer(v, new HttpClient());
|
||||||
|
return View(new AddToPlaylistContext
|
||||||
|
{
|
||||||
|
Id = v,
|
||||||
|
Video = await _youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||||
|
Playlists = await DatabaseManager.Playlists.GetUserPlaylists(user.UserID),
|
||||||
|
Thumbnail = ytPlayer?["videoDetails"]?["thumbnail"]?["thumbnails"]?[0]?["url"]?.ToString() ?? $"https://i.ytimg.com/vi_webp/{v}/maxresdefault.webp",
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult CreatePlaylist(string returnUrl = null)
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||||
|
Redirect("/Account/Login");
|
||||||
|
|
||||||
|
return View(new BaseContext
|
||||||
|
{
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreatePlaylist()
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||||
|
Redirect("/Account/Login");
|
||||||
|
|
||||||
|
if (!Request.Form.ContainsKey("name") || string.IsNullOrWhiteSpace(Request.Form["name"])) return BadRequest();
|
||||||
|
|
||||||
|
LTPlaylist pl = await DatabaseManager.Playlists.CreatePlaylist(
|
||||||
|
user,
|
||||||
|
Request.Form["name"],
|
||||||
|
string.IsNullOrWhiteSpace(Request.Form["description"]) ? "" : Request.Form["description"],
|
||||||
|
Enum.Parse<PlaylistVisibility>(string.IsNullOrWhiteSpace(Request.Form["visibility"]) ? "UNLISTED" : Request.Form["visibility"]));
|
||||||
|
|
||||||
|
return Redirect($"/playlist?list={pl.Id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
187
core/LightTube/Controllers/ApiController.cs
Normal file
187
core/LightTube/Controllers/ApiController.cs
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml;
|
||||||
|
using InnerTube;
|
||||||
|
using InnerTube.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace LightTube.Controllers
|
||||||
|
{
|
||||||
|
[Route("/api")]
|
||||||
|
public class ApiController : Controller
|
||||||
|
{
|
||||||
|
private const string VideoIdRegex = @"[a-zA-Z0-9_-]{11}";
|
||||||
|
private const string ChannelIdRegex = @"[a-zA-Z0-9_-]{24}";
|
||||||
|
private const string PlaylistIdRegex = @"[a-zA-Z0-9_-]{34}";
|
||||||
|
private readonly Youtube _youtube;
|
||||||
|
|
||||||
|
public ApiController(Youtube youtube)
|
||||||
|
{
|
||||||
|
_youtube = youtube;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IActionResult Xml(XmlNode xmlDocument)
|
||||||
|
{
|
||||||
|
MemoryStream ms = new();
|
||||||
|
ms.Write(Encoding.UTF8.GetBytes(xmlDocument.OuterXml));
|
||||||
|
ms.Position = 0;
|
||||||
|
HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||||
|
return File(ms, "application/xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("player")]
|
||||||
|
public async Task<IActionResult> GetPlayerInfo(string v)
|
||||||
|
{
|
||||||
|
if (v is null)
|
||||||
|
return GetErrorVideoPlayer("", "Missing YouTube ID (query parameter `v`)");
|
||||||
|
|
||||||
|
Regex regex = new(VideoIdRegex);
|
||||||
|
if (!regex.IsMatch(v) || v.Length != 11)
|
||||||
|
return GetErrorVideoPlayer(v, "Invalid YouTube ID " + v);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
YoutubePlayer player =
|
||||||
|
await _youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion());
|
||||||
|
XmlDocument xml = player.GetXmlDocument();
|
||||||
|
return Xml(xml);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return GetErrorVideoPlayer(v, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IActionResult GetErrorVideoPlayer(string videoId, string message)
|
||||||
|
{
|
||||||
|
YoutubePlayer player = new()
|
||||||
|
{
|
||||||
|
Id = videoId,
|
||||||
|
Title = "",
|
||||||
|
Description = "",
|
||||||
|
Tags = Array.Empty<string>(),
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = "",
|
||||||
|
Id = "",
|
||||||
|
Avatars = Array.Empty<Thumbnail>()
|
||||||
|
},
|
||||||
|
Duration = 0,
|
||||||
|
Chapters = Array.Empty<Chapter>(),
|
||||||
|
Thumbnails = Array.Empty<Thumbnail>(),
|
||||||
|
Formats = Array.Empty<Format>(),
|
||||||
|
AdaptiveFormats = Array.Empty<Format>(),
|
||||||
|
Subtitles = Array.Empty<Subtitle>(),
|
||||||
|
Storyboards = Array.Empty<string>(),
|
||||||
|
ExpiresInSeconds = "0",
|
||||||
|
ErrorMessage = message
|
||||||
|
};
|
||||||
|
return Xml(player.GetXmlDocument());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("video")]
|
||||||
|
public async Task<IActionResult> GetVideoInfo(string v)
|
||||||
|
{
|
||||||
|
if (v is null)
|
||||||
|
return GetErrorVideoPlayer("", "Missing YouTube ID (query parameter `v`)");
|
||||||
|
|
||||||
|
Regex regex = new(VideoIdRegex);
|
||||||
|
if (!regex.IsMatch(v) || v.Length != 11)
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement item = doc.CreateElement("Error");
|
||||||
|
|
||||||
|
item.InnerText = "Invalid YouTube ID " + v;
|
||||||
|
|
||||||
|
doc.AppendChild(item);
|
||||||
|
return Xml(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
YoutubeVideo player = await _youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion());
|
||||||
|
XmlDocument xml = player.GetXmlDocument();
|
||||||
|
return Xml(xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("search")]
|
||||||
|
public async Task<IActionResult> Search(string query, string continuation = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(query) && string.IsNullOrWhiteSpace(continuation))
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement item = doc.CreateElement("Error");
|
||||||
|
|
||||||
|
item.InnerText = "Invalid query " + query;
|
||||||
|
|
||||||
|
doc.AppendChild(item);
|
||||||
|
return Xml(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
YoutubeSearchResults player = await _youtube.SearchAsync(query, continuation, HttpContext.GetLanguage(),
|
||||||
|
HttpContext.GetRegion());
|
||||||
|
XmlDocument xml = player.GetXmlDocument();
|
||||||
|
return Xml(xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("playlist")]
|
||||||
|
public async Task<IActionResult> Playlist(string id, string continuation = null)
|
||||||
|
{
|
||||||
|
Regex regex = new(PlaylistIdRegex);
|
||||||
|
if (!regex.IsMatch(id) || id.Length != 34) return GetErrorVideoPlayer(id, "Invalid playlist ID " + id);
|
||||||
|
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(id) && string.IsNullOrWhiteSpace(continuation))
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement item = doc.CreateElement("Error");
|
||||||
|
|
||||||
|
item.InnerText = "Invalid ID " + id;
|
||||||
|
|
||||||
|
doc.AppendChild(item);
|
||||||
|
return Xml(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
YoutubePlaylist player = await _youtube.GetPlaylistAsync(id, continuation, HttpContext.GetLanguage(),
|
||||||
|
HttpContext.GetRegion());
|
||||||
|
XmlDocument xml = player.GetXmlDocument();
|
||||||
|
return Xml(xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("channel")]
|
||||||
|
public async Task<IActionResult> Channel(string id, ChannelTabs tab = ChannelTabs.Home,
|
||||||
|
string continuation = null)
|
||||||
|
{
|
||||||
|
Regex regex = new(ChannelIdRegex);
|
||||||
|
if (!regex.IsMatch(id) || id.Length != 24) return GetErrorVideoPlayer(id, "Invalid channel ID " + id);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(id) && string.IsNullOrWhiteSpace(continuation))
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement item = doc.CreateElement("Error");
|
||||||
|
|
||||||
|
item.InnerText = "Invalid ID " + id;
|
||||||
|
|
||||||
|
doc.AppendChild(item);
|
||||||
|
return Xml(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
YoutubeChannel player = await _youtube.GetChannelAsync(id, tab, continuation, HttpContext.GetLanguage(),
|
||||||
|
HttpContext.GetRegion());
|
||||||
|
XmlDocument xml = player.GetXmlDocument();
|
||||||
|
return Xml(xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("trending")]
|
||||||
|
public async Task<IActionResult> Trending(string id, string continuation = null)
|
||||||
|
{
|
||||||
|
YoutubeTrends player = await _youtube.GetExploreAsync(id, continuation,
|
||||||
|
HttpContext.GetLanguage(),
|
||||||
|
HttpContext.GetRegion());
|
||||||
|
XmlDocument xml = player.GetXmlDocument();
|
||||||
|
return Xml(xml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
189
core/LightTube/Controllers/AuthorizedApiController.cs
Normal file
189
core/LightTube/Controllers/AuthorizedApiController.cs
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml;
|
||||||
|
using InnerTube;
|
||||||
|
using InnerTube.Models;
|
||||||
|
using LightTube.Database;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace LightTube.Controllers
|
||||||
|
{
|
||||||
|
[Route("/api/auth")]
|
||||||
|
public class AuthorizedApiController : Controller
|
||||||
|
{
|
||||||
|
private readonly Youtube _youtube;
|
||||||
|
|
||||||
|
private IReadOnlyList<string> _scopes = new[]
|
||||||
|
{
|
||||||
|
"api.subscriptions.read",
|
||||||
|
"api.subscriptions.write"
|
||||||
|
};
|
||||||
|
|
||||||
|
public AuthorizedApiController(Youtube youtube)
|
||||||
|
{
|
||||||
|
_youtube = youtube;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IActionResult Xml(XmlNode xmlDocument, HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
MemoryStream ms = new();
|
||||||
|
ms.Write(Encoding.UTF8.GetBytes(xmlDocument.OuterXml));
|
||||||
|
ms.Position = 0;
|
||||||
|
HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||||
|
Response.StatusCode = (int)statusCode;
|
||||||
|
return File(ms, "application/xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
private XmlNode BuildErrorXml(string message)
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement error = doc.CreateElement("Error");
|
||||||
|
error.InnerText = message;
|
||||||
|
doc.AppendChild(error);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("getToken")]
|
||||||
|
public async Task<IActionResult> GetToken()
|
||||||
|
{
|
||||||
|
if (!Request.Headers.TryGetValue("User-Agent", out StringValues userAgent))
|
||||||
|
return Xml(BuildErrorXml("Missing User-Agent header"), HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
|
Match match = Regex.Match(userAgent.ToString(), DatabaseManager.ApiUaRegex);
|
||||||
|
if (!match.Success)
|
||||||
|
return Xml(BuildErrorXml("Bad User-Agent header. Please see 'Documentation/API requests'"), HttpStatusCode.BadRequest);
|
||||||
|
if (match.Groups[1].ToString() != "1.0")
|
||||||
|
return Xml(BuildErrorXml($"Unknown API version {match.Groups[1]}"), HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
|
if (!Request.Form.TryGetValue("user", out StringValues user))
|
||||||
|
return Xml(BuildErrorXml("Missing request value: 'user'"), HttpStatusCode.BadRequest);
|
||||||
|
if (!Request.Form.TryGetValue("password", out StringValues password))
|
||||||
|
return Xml(BuildErrorXml("Missing request value: 'password'"), HttpStatusCode.BadRequest);
|
||||||
|
if (!Request.Form.TryGetValue("scopes", out StringValues scopes))
|
||||||
|
return Xml(BuildErrorXml("Missing request value: 'scopes'"), HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
|
string[] newScopes = scopes.First().Split(",");
|
||||||
|
foreach (string s in newScopes)
|
||||||
|
if (!_scopes.Contains(s))
|
||||||
|
return Xml(BuildErrorXml($"Unknown scope '{s}'"), HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LTLogin ltLogin =
|
||||||
|
await DatabaseManager.Logins.CreateToken(user, password, userAgent.ToString(),
|
||||||
|
scopes.First().Split(","));
|
||||||
|
return Xml(ltLogin.GetXmlElement(), HttpStatusCode.Created);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return Xml(BuildErrorXml("Invalid credentials"), HttpStatusCode.Unauthorized);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
return Xml(BuildErrorXml("User has API access disabled"), HttpStatusCode.Forbidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("subscriptions/feed")]
|
||||||
|
public async Task<IActionResult> SubscriptionsFeed()
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "api.subscriptions.read"))
|
||||||
|
return Xml(BuildErrorXml("Unauthorized"), HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
|
SubscriptionFeed feed = new()
|
||||||
|
{
|
||||||
|
videos = await YoutubeRSS.GetMultipleFeeds(user.SubscribedChannels)
|
||||||
|
};
|
||||||
|
|
||||||
|
return Xml(feed.GetXmlDocument(), HttpStatusCode.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("subscriptions/channels")]
|
||||||
|
public IActionResult SubscriptionsChannels()
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "api.subscriptions.read"))
|
||||||
|
return Xml(BuildErrorXml("Unauthorized"), HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
|
SubscriptionChannels feed = new()
|
||||||
|
{
|
||||||
|
Channels = user.SubscribedChannels.Select(DatabaseManager.Channels.GetChannel).ToArray()
|
||||||
|
};
|
||||||
|
Array.Sort(feed.Channels, (p, q) => string.Compare(p.Name, q.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
return Xml(feed.GetXmlDocument(), HttpStatusCode.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
[Route("subscriptions/channels")]
|
||||||
|
public async Task<IActionResult> Subscribe()
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "api.subscriptions.write"))
|
||||||
|
return Xml(BuildErrorXml("Unauthorized"), HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
|
Request.Form.TryGetValue("id", out StringValues ids);
|
||||||
|
string id = ids.ToString();
|
||||||
|
|
||||||
|
if (user.SubscribedChannels.Contains(id))
|
||||||
|
return StatusCode((int)HttpStatusCode.NotModified);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
YoutubeChannel channel = await _youtube.GetChannelAsync(id);
|
||||||
|
|
||||||
|
if (channel.Id is null)
|
||||||
|
return StatusCode((int)HttpStatusCode.NotFound);
|
||||||
|
|
||||||
|
(LTChannel ltChannel, bool _) = await DatabaseManager.Logins.SubscribeToChannel(user, channel);
|
||||||
|
|
||||||
|
XmlDocument doc = new();
|
||||||
|
doc.AppendChild(ltChannel.GetXmlElement(doc));
|
||||||
|
return Xml(doc, HttpStatusCode.OK);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Xml(BuildErrorXml(e.Message), HttpStatusCode.InternalServerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
[Route("subscriptions/channels")]
|
||||||
|
public async Task<IActionResult> Unsubscribe()
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "api.subscriptions.write"))
|
||||||
|
return Xml(BuildErrorXml("Unauthorized"), HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
|
Request.Form.TryGetValue("id", out StringValues ids);
|
||||||
|
string id = ids.ToString();
|
||||||
|
|
||||||
|
if (!user.SubscribedChannels.Contains(id))
|
||||||
|
return StatusCode((int)HttpStatusCode.NotModified);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
YoutubeChannel channel = await _youtube.GetChannelAsync(id);
|
||||||
|
|
||||||
|
if (channel.Id is null)
|
||||||
|
return StatusCode((int)HttpStatusCode.NotFound);
|
||||||
|
|
||||||
|
(LTChannel ltChannel, bool _) = await DatabaseManager.Logins.SubscribeToChannel(user, channel);
|
||||||
|
|
||||||
|
XmlDocument doc = new();
|
||||||
|
doc.AppendChild(ltChannel.GetXmlElement(doc));
|
||||||
|
return Xml(doc, HttpStatusCode.OK);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Xml(BuildErrorXml(e.Message), HttpStatusCode.InternalServerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
104
core/LightTube/Controllers/FeedController.cs
Normal file
104
core/LightTube/Controllers/FeedController.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LightTube.Contexts;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using InnerTube;
|
||||||
|
using LightTube.Database;
|
||||||
|
|
||||||
|
namespace LightTube.Controllers
|
||||||
|
{
|
||||||
|
[Route("/feed")]
|
||||||
|
public class FeedController : Controller
|
||||||
|
{
|
||||||
|
private readonly ILogger<FeedController> _logger;
|
||||||
|
private readonly Youtube _youtube;
|
||||||
|
|
||||||
|
public FeedController(ILogger<FeedController> logger, Youtube youtube)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_youtube = youtube;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("subscriptions")]
|
||||||
|
public async Task<IActionResult> Subscriptions()
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||||
|
return Redirect("/Account/Login");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FeedContext context = new()
|
||||||
|
{
|
||||||
|
Channels = user.SubscribedChannels.Select(DatabaseManager.Channels.GetChannel).ToArray(),
|
||||||
|
Videos = await YoutubeRSS.GetMultipleFeeds(user.SubscribedChannels),
|
||||||
|
RssToken = user.RssToken,
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
};
|
||||||
|
Array.Sort(context.Channels, (p, q) => string.Compare(p.Name, q.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
return View(context);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
HttpContext.Response.Cookies.Delete("token");
|
||||||
|
return Redirect("/Account/Login");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("channels")]
|
||||||
|
public IActionResult Channels()
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||||
|
return Redirect("/Account/Login");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FeedContext context = new()
|
||||||
|
{
|
||||||
|
Channels = user.SubscribedChannels.Select(DatabaseManager.Channels.GetChannel).ToArray(),
|
||||||
|
Videos = null,
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
};
|
||||||
|
Array.Sort(context.Channels, (p, q) => string.Compare(p.Name, q.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
return View(context);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
HttpContext.Response.Cookies.Delete("token");
|
||||||
|
return Redirect("/Account/Login");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("explore")]
|
||||||
|
public IActionResult Explore()
|
||||||
|
{
|
||||||
|
return View(new BaseContext
|
||||||
|
{
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("/feed/library")]
|
||||||
|
public async Task<IActionResult> Playlists()
|
||||||
|
{
|
||||||
|
if (!HttpContext.TryGetUser(out LTUser user, "web"))
|
||||||
|
Redirect("/Account/Login");
|
||||||
|
|
||||||
|
return View(new PlaylistsContext
|
||||||
|
{
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request),
|
||||||
|
Playlists = await DatabaseManager.Playlists.GetUserPlaylists(user.UserID)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("/rss")]
|
||||||
|
public async Task<IActionResult> Playlists(string token, int limit = 15)
|
||||||
|
{
|
||||||
|
if (!DatabaseManager.TryGetRssUser(token, out LTUser user))
|
||||||
|
return Unauthorized();
|
||||||
|
return File(Encoding.UTF8.GetBytes(await user.GenerateRssFeed(Request.Host.ToString(), Math.Clamp(limit, 0, 50))), "application/xml");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
core/LightTube/Controllers/HomeController.cs
Normal file
50
core/LightTube/Controllers/HomeController.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LightTube.Contexts;
|
||||||
|
using LightTube.Models;
|
||||||
|
using Microsoft.AspNetCore.Diagnostics;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using InnerTube;
|
||||||
|
using InnerTube.Models;
|
||||||
|
using ErrorContext = LightTube.Contexts.ErrorContext;
|
||||||
|
|
||||||
|
namespace LightTube.Controllers
|
||||||
|
{
|
||||||
|
public class HomeController : Controller
|
||||||
|
{
|
||||||
|
private readonly ILogger<HomeController> _logger;
|
||||||
|
private readonly Youtube _youtube;
|
||||||
|
|
||||||
|
public HomeController(ILogger<HomeController> logger, Youtube youtube)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_youtube = youtube;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult Index()
|
||||||
|
{
|
||||||
|
return View(new BaseContext
|
||||||
|
{
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||||
|
public IActionResult Error()
|
||||||
|
{
|
||||||
|
return View(new ErrorContext
|
||||||
|
{
|
||||||
|
Path = HttpContext.Features.Get<IExceptionHandlerPathFeature>().Path,
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
core/LightTube/Controllers/ManifestController.cs
Normal file
62
core/LightTube/Controllers/ManifestController.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using InnerTube;
|
||||||
|
using InnerTube.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace LightTube.Controllers
|
||||||
|
{
|
||||||
|
[Route("/manifest")]
|
||||||
|
public class ManifestController : Controller
|
||||||
|
{
|
||||||
|
private readonly Youtube _youtube;
|
||||||
|
private readonly HttpClient _client = new();
|
||||||
|
|
||||||
|
public ManifestController(Youtube youtube)
|
||||||
|
{
|
||||||
|
_youtube = youtube;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("{v}")]
|
||||||
|
public async Task<IActionResult> DefaultManifest(string v)
|
||||||
|
{
|
||||||
|
YoutubePlayer player = await _youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion());
|
||||||
|
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||||
|
return StatusCode(500, player.ErrorMessage);
|
||||||
|
return Redirect(player.IsLive ? $"/manifest/{v}.m3u8" : $"/manifest/{v}.mpd" + Request.QueryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("{v}.mpd")]
|
||||||
|
public async Task<IActionResult> DashManifest(string v, string videoCodec = null, string audioCodec = null, bool useProxy = true)
|
||||||
|
{
|
||||||
|
YoutubePlayer player = await _youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion());
|
||||||
|
string manifest = player.GetMpdManifest(useProxy ? $"https://{Request.Host}/proxy/" : null, videoCodec, audioCodec);
|
||||||
|
return File(Encoding.UTF8.GetBytes(manifest), "application/dash+xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("{v}.m3u8")]
|
||||||
|
public async Task<IActionResult> HlsManifest(string v, bool useProxy = true)
|
||||||
|
{
|
||||||
|
YoutubePlayer player = await _youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion(), true);
|
||||||
|
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||||
|
return StatusCode(403, player.ErrorMessage);
|
||||||
|
|
||||||
|
if (player.IsLive)
|
||||||
|
{
|
||||||
|
string manifest = await player.GetHlsManifest(useProxy ? $"https://{Request.Host}/proxy" : null);
|
||||||
|
return File(Encoding.UTF8.GetBytes(manifest), "application/vnd.apple.mpegurl");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useProxy)
|
||||||
|
return StatusCode(400, "HLS proxy for non-live videos are not supported at the moment.");
|
||||||
|
return Redirect(player.HlsManifestUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
517
core/LightTube/Controllers/ProxyController.cs
Normal file
517
core/LightTube/Controllers/ProxyController.cs
Normal file
@ -0,0 +1,517 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using InnerTube;
|
||||||
|
using InnerTube.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace LightTube.Controllers
|
||||||
|
{
|
||||||
|
[Route("/proxy")]
|
||||||
|
public class ProxyController : Controller
|
||||||
|
{
|
||||||
|
private readonly ILogger<YoutubeController> _logger;
|
||||||
|
private readonly Youtube _youtube;
|
||||||
|
private string[] BlockedHeaders =
|
||||||
|
{
|
||||||
|
"host",
|
||||||
|
"cookies"
|
||||||
|
};
|
||||||
|
|
||||||
|
public ProxyController(ILogger<YoutubeController> logger, Youtube youtube)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_youtube = youtube;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("media/{videoId}/{formatId}")]
|
||||||
|
public async Task Media(string videoId, string formatId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
|
||||||
|
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||||
|
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(player.ErrorMessage));
|
||||||
|
await Response.StartAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Format> formats = new();
|
||||||
|
|
||||||
|
formats.AddRange(player.Formats);
|
||||||
|
formats.AddRange(player.AdaptiveFormats);
|
||||||
|
|
||||||
|
if (!formats.Any(x => x.FormatId == formatId))
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.NotFound;
|
||||||
|
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(
|
||||||
|
$"Format with ID {formatId} not found.\nAvailable IDs are: {string.Join(", ", formats.Select(x => x.FormatId.ToString()))}"));
|
||||||
|
await Response.StartAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string url = formats.First(x => x.FormatId == formatId).Url;
|
||||||
|
|
||||||
|
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||||
|
url = "https://" + url;
|
||||||
|
|
||||||
|
HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
request.Method = Request.Method;
|
||||||
|
|
||||||
|
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||||
|
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||||
|
foreach (string value in values)
|
||||||
|
request.Headers.Add(header, value);
|
||||||
|
|
||||||
|
HttpWebResponse response;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = (HttpWebResponse) request.GetResponse();
|
||||||
|
}
|
||||||
|
catch (WebException e)
|
||||||
|
{
|
||||||
|
response = e.Response as HttpWebResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response == null)
|
||||||
|
await Response.StartAsync();
|
||||||
|
|
||||||
|
foreach (string header in response.Headers.AllKeys)
|
||||||
|
if (Response.Headers.ContainsKey(header))
|
||||||
|
Response.Headers[header] = response.Headers.Get(header);
|
||||||
|
else
|
||||||
|
Response.Headers.Add(header, response.Headers.Get(header));
|
||||||
|
Response.StatusCode = (int) response.StatusCode;
|
||||||
|
|
||||||
|
await using Stream stream = response.GetResponseStream();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await stream.CopyToAsync(Response.Body, HttpContext.RequestAborted);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// an exception is thrown if the client suddenly stops streaming
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response.StartAsync();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||||
|
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(e.ToString()));
|
||||||
|
await Response.StartAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("download/{videoId}/{formatId}/{filename}")]
|
||||||
|
public async Task Download(string videoId, string formatId, string filename)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
|
||||||
|
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||||
|
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(player.ErrorMessage));
|
||||||
|
await Response.StartAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Format> formats = new();
|
||||||
|
|
||||||
|
formats.AddRange(player.Formats);
|
||||||
|
formats.AddRange(player.AdaptiveFormats);
|
||||||
|
|
||||||
|
if (!formats.Any(x => x.FormatId == formatId))
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.NotFound;
|
||||||
|
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(
|
||||||
|
$"Format with ID {formatId} not found.\nAvailable IDs are: {string.Join(", ", formats.Select(x => x.FormatId.ToString()))}"));
|
||||||
|
await Response.StartAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string url = formats.First(x => x.FormatId == formatId).Url;
|
||||||
|
|
||||||
|
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||||
|
url = "https://" + url;
|
||||||
|
|
||||||
|
HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
request.Method = Request.Method;
|
||||||
|
|
||||||
|
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||||
|
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||||
|
foreach (string value in values)
|
||||||
|
request.Headers.Add(header, value);
|
||||||
|
|
||||||
|
HttpWebResponse response;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = (HttpWebResponse) request.GetResponse();
|
||||||
|
}
|
||||||
|
catch (WebException e)
|
||||||
|
{
|
||||||
|
response = e.Response as HttpWebResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response == null)
|
||||||
|
await Response.StartAsync();
|
||||||
|
|
||||||
|
foreach (string header in response.Headers.AllKeys)
|
||||||
|
if (Response.Headers.ContainsKey(header))
|
||||||
|
Response.Headers[header] = response.Headers.Get(header);
|
||||||
|
else
|
||||||
|
Response.Headers.Add(header, response.Headers.Get(header));
|
||||||
|
Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{Regex.Replace(filename, @"[^\u0000-\u007F]+", string.Empty)}\"");
|
||||||
|
Response.StatusCode = (int) response.StatusCode;
|
||||||
|
|
||||||
|
await using Stream stream = response.GetResponseStream();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await stream.CopyToAsync(Response.Body, HttpContext.RequestAborted);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// an exception is thrown if the client suddenly stops streaming
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response.StartAsync();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||||
|
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(e.ToString()));
|
||||||
|
await Response.StartAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("caption/{videoId}/{language}")]
|
||||||
|
public async Task<FileStreamResult> SubtitleProxy(string videoId, string language)
|
||||||
|
{
|
||||||
|
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
|
||||||
|
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||||
|
return File(new MemoryStream(Encoding.UTF8.GetBytes(player.ErrorMessage)),
|
||||||
|
"text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
string url = null;
|
||||||
|
Subtitle? subtitle = player.Subtitles.FirstOrDefault(x => string.Equals(x.Language, language, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
if (subtitle is null)
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.NotFound;
|
||||||
|
return File(
|
||||||
|
new MemoryStream(Encoding.UTF8.GetBytes(
|
||||||
|
$"There are no available subtitles for {language}. Available language codes are: {string.Join(", ", player.Subtitles.Select(x => $"\"{x.Language}\""))}")),
|
||||||
|
"text/plain");
|
||||||
|
}
|
||||||
|
url = subtitle.Url.Replace("fmt=srv3", "fmt=vtt");
|
||||||
|
|
||||||
|
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||||
|
url = "https://" + url;
|
||||||
|
|
||||||
|
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
|
||||||
|
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||||
|
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||||
|
foreach (string value in values)
|
||||||
|
request.Headers.Add(header, value);
|
||||||
|
|
||||||
|
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||||
|
|
||||||
|
await using Stream stream = response.GetResponseStream();
|
||||||
|
using StreamReader reader = new(stream);
|
||||||
|
|
||||||
|
return File(new MemoryStream(Encoding.UTF8.GetBytes(await reader.ReadToEndAsync())),
|
||||||
|
"text/vtt");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("image")]
|
||||||
|
[Obsolete("Use /proxy/thumbnail instead")]
|
||||||
|
public async Task ImageProxy(string url)
|
||||||
|
{
|
||||||
|
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||||
|
url = "https://" + url;
|
||||||
|
|
||||||
|
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
|
||||||
|
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||||
|
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||||
|
foreach (string value in values)
|
||||||
|
request.Headers.Add(header, value);
|
||||||
|
|
||||||
|
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||||
|
|
||||||
|
foreach (string header in response.Headers.AllKeys)
|
||||||
|
if (Response.Headers.ContainsKey(header))
|
||||||
|
Response.Headers[header] = response.Headers.Get(header);
|
||||||
|
else
|
||||||
|
Response.Headers.Add(header, response.Headers.Get(header));
|
||||||
|
Response.StatusCode = (int)response.StatusCode;
|
||||||
|
|
||||||
|
await using Stream stream = response.GetResponseStream();
|
||||||
|
await stream.CopyToAsync(Response.Body);
|
||||||
|
await Response.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("thumbnail/{videoId}/{index:int}")]
|
||||||
|
public async Task ThumbnailProxy(string videoId, int index = 0)
|
||||||
|
{
|
||||||
|
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
|
||||||
|
if (index == -1) index = player.Thumbnails.Length - 1;
|
||||||
|
if (index >= player.Thumbnails.Length)
|
||||||
|
{
|
||||||
|
Response.StatusCode = 404;
|
||||||
|
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(
|
||||||
|
$"Cannot find thumbnail #{index} for {videoId}. The maximum quality is {player.Thumbnails.Length - 1}"));
|
||||||
|
await Response.StartAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string url = player.Thumbnails.FirstOrDefault()?.Url;
|
||||||
|
|
||||||
|
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||||
|
url = "https://" + url;
|
||||||
|
|
||||||
|
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
|
||||||
|
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||||
|
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||||
|
foreach (string value in values)
|
||||||
|
request.Headers.Add(header, value);
|
||||||
|
|
||||||
|
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||||
|
|
||||||
|
foreach (string header in response.Headers.AllKeys)
|
||||||
|
if (Response.Headers.ContainsKey(header))
|
||||||
|
Response.Headers[header] = response.Headers.Get(header);
|
||||||
|
else
|
||||||
|
Response.Headers.Add(header, response.Headers.Get(header));
|
||||||
|
Response.StatusCode = (int)response.StatusCode;
|
||||||
|
|
||||||
|
await using Stream stream = response.GetResponseStream();
|
||||||
|
await stream.CopyToAsync(Response.Body);
|
||||||
|
await Response.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("storyboard/{videoId}")]
|
||||||
|
public async Task StoryboardProxy(string videoId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
|
||||||
|
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||||
|
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(player.ErrorMessage));
|
||||||
|
await Response.StartAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!player.Storyboards.Any())
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.NotFound;
|
||||||
|
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes("No usable storyboard found."));
|
||||||
|
await Response.StartAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string url = player.Storyboards.First();
|
||||||
|
|
||||||
|
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
|
||||||
|
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||||
|
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||||
|
foreach (string value in values)
|
||||||
|
request.Headers.Add(header, value);
|
||||||
|
|
||||||
|
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||||
|
|
||||||
|
foreach (string header in response.Headers.AllKeys)
|
||||||
|
if (Response.Headers.ContainsKey(header))
|
||||||
|
Response.Headers[header] = response.Headers.Get(header);
|
||||||
|
else
|
||||||
|
Response.Headers.Add(header, response.Headers.Get(header));
|
||||||
|
Response.StatusCode = (int)response.StatusCode;
|
||||||
|
|
||||||
|
await using Stream stream = response.GetResponseStream();
|
||||||
|
await stream.CopyToAsync(Response.Body);
|
||||||
|
await Response.StartAsync();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||||
|
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(e.ToString()));
|
||||||
|
await Response.StartAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("hls")]
|
||||||
|
public async Task<IActionResult> HlsProxy(string url)
|
||||||
|
{
|
||||||
|
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||||
|
url = "https://" + url;
|
||||||
|
|
||||||
|
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
|
||||||
|
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||||
|
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||||
|
foreach (string value in values)
|
||||||
|
request.Headers.Add(header, value);
|
||||||
|
|
||||||
|
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||||
|
|
||||||
|
await using Stream stream = response.GetResponseStream();
|
||||||
|
using StreamReader reader = new(stream);
|
||||||
|
string manifest = await reader.ReadToEndAsync();
|
||||||
|
StringBuilder proxyManifest = new ();
|
||||||
|
|
||||||
|
foreach (string s in manifest.Split("\n"))
|
||||||
|
{
|
||||||
|
// also check if proxy enabled
|
||||||
|
proxyManifest.AppendLine(!s.StartsWith("http")
|
||||||
|
? s
|
||||||
|
: $"https://{Request.Host}/proxy/video?url={HttpUtility.UrlEncode(s)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return File(new MemoryStream(Encoding.UTF8.GetBytes(proxyManifest.ToString())),
|
||||||
|
"application/vnd.apple.mpegurl");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("manifest/{videoId}")]
|
||||||
|
public async Task<IActionResult> ManifestProxy(string videoId, string formatId, bool useProxy = true)
|
||||||
|
{
|
||||||
|
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId, iOS: true);
|
||||||
|
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||||
|
return File(new MemoryStream(Encoding.UTF8.GetBytes(player.ErrorMessage)),
|
||||||
|
"text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.HlsManifestUrl == null)
|
||||||
|
{
|
||||||
|
Response.StatusCode = (int) HttpStatusCode.NotFound;
|
||||||
|
return File(new MemoryStream(Encoding.UTF8.GetBytes("This video does not have an HLS manifest URL")),
|
||||||
|
"text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
string url = player.HlsManifestUrl;
|
||||||
|
|
||||||
|
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||||
|
url = "https://" + url;
|
||||||
|
|
||||||
|
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
|
||||||
|
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||||
|
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||||
|
foreach (string value in values)
|
||||||
|
request.Headers.Add(header, value);
|
||||||
|
|
||||||
|
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||||
|
|
||||||
|
await using Stream stream = response.GetResponseStream();
|
||||||
|
using StreamReader reader = new(stream);
|
||||||
|
string manifest = await reader.ReadToEndAsync();
|
||||||
|
StringBuilder proxyManifest = new ();
|
||||||
|
|
||||||
|
if (useProxy)
|
||||||
|
foreach (string s in manifest.Split("\n"))
|
||||||
|
{
|
||||||
|
// also check if proxy enabled
|
||||||
|
proxyManifest.AppendLine(!s.StartsWith("http")
|
||||||
|
? s
|
||||||
|
: $"https://{Request.Host}/proxy/ytmanifest?path=" + HttpUtility.UrlEncode(s[46..]));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
proxyManifest.Append(manifest);
|
||||||
|
|
||||||
|
return File(new MemoryStream(Encoding.UTF8.GetBytes(proxyManifest.ToString())),
|
||||||
|
"application/vnd.apple.mpegurl");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("ytmanifest")]
|
||||||
|
public async Task<IActionResult> YoutubeManifestProxy(string path)
|
||||||
|
{
|
||||||
|
string url = "https://manifest.googlevideo.com" + path;
|
||||||
|
StringBuilder sb = new();
|
||||||
|
|
||||||
|
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||||
|
url = "https://" + url;
|
||||||
|
|
||||||
|
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
|
||||||
|
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||||
|
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||||
|
foreach (string value in values)
|
||||||
|
request.Headers.Add(header, value);
|
||||||
|
|
||||||
|
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||||
|
|
||||||
|
await using Stream stream = response.GetResponseStream();
|
||||||
|
using StreamReader reader = new(stream);
|
||||||
|
string manifest = await reader.ReadToEndAsync();
|
||||||
|
|
||||||
|
foreach (string line in manifest.Split("\n"))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
sb.AppendLine();
|
||||||
|
else if (line.StartsWith("#"))
|
||||||
|
sb.AppendLine(line);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Uri u = new(line);
|
||||||
|
sb.AppendLine($"https://{Request.Host}/proxy/videoplayback?host={u.Host}&path={HttpUtility.UrlEncode(u.PathAndQuery)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return File(new MemoryStream(Encoding.UTF8.GetBytes(sb.ToString())),
|
||||||
|
"application/vnd.apple.mpegurl");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("videoplayback")]
|
||||||
|
public async Task VideoPlaybackProxy(string path, string host)
|
||||||
|
{
|
||||||
|
// make sure this is only used in livestreams
|
||||||
|
|
||||||
|
string url = $"https://{host}{path}";
|
||||||
|
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
|
||||||
|
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
|
||||||
|
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
|
||||||
|
foreach (string value in values)
|
||||||
|
request.Headers.Add(header, value);
|
||||||
|
|
||||||
|
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
|
||||||
|
|
||||||
|
await using Stream stream = response.GetResponseStream();
|
||||||
|
|
||||||
|
Response.ContentType = "application/octet-stream";
|
||||||
|
await Response.StartAsync();
|
||||||
|
await stream.CopyToAsync(Response.Body, HttpContext.RequestAborted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
67
core/LightTube/Controllers/TogglesController.cs
Normal file
67
core/LightTube/Controllers/TogglesController.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace LightTube.Controllers
|
||||||
|
{
|
||||||
|
[Route("/toggles")]
|
||||||
|
public class TogglesController : Controller
|
||||||
|
{
|
||||||
|
[Route("theme")]
|
||||||
|
public IActionResult ToggleTheme(string redirectUrl)
|
||||||
|
{
|
||||||
|
if (Request.Cookies.TryGetValue("theme", out string theme))
|
||||||
|
Response.Cookies.Append("theme", theme switch
|
||||||
|
{
|
||||||
|
"light" => "dark",
|
||||||
|
"dark" => "light",
|
||||||
|
var _ => "dark"
|
||||||
|
}, new CookieOptions
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.MaxValue
|
||||||
|
});
|
||||||
|
else
|
||||||
|
Response.Cookies.Append("theme", "light");
|
||||||
|
|
||||||
|
return Redirect(redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("compatibility")]
|
||||||
|
public IActionResult ToggleCompatibility(string redirectUrl)
|
||||||
|
{
|
||||||
|
if (Request.Cookies.TryGetValue("compatibility", out string compatibility))
|
||||||
|
Response.Cookies.Append("compatibility", compatibility switch
|
||||||
|
{
|
||||||
|
"true" => "false",
|
||||||
|
"false" => "true",
|
||||||
|
var _ => "true"
|
||||||
|
}, new CookieOptions
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.MaxValue
|
||||||
|
});
|
||||||
|
else
|
||||||
|
Response.Cookies.Append("compatibility", "true");
|
||||||
|
|
||||||
|
return Redirect(redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("collapse_guide")]
|
||||||
|
public IActionResult ToggleCollapseGuide(string redirectUrl)
|
||||||
|
{
|
||||||
|
if (Request.Cookies.TryGetValue("minmode", out string minmode))
|
||||||
|
Response.Cookies.Append("minmode", minmode switch
|
||||||
|
{
|
||||||
|
"true" => "false",
|
||||||
|
"false" => "true",
|
||||||
|
var _ => "true"
|
||||||
|
}, new CookieOptions
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.MaxValue
|
||||||
|
});
|
||||||
|
else
|
||||||
|
Response.Cookies.Append("minmode", "true");
|
||||||
|
|
||||||
|
return Redirect(redirectUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
226
core/LightTube/Controllers/YoutubeController.cs
Normal file
226
core/LightTube/Controllers/YoutubeController.cs
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LightTube.Contexts;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using InnerTube;
|
||||||
|
using InnerTube.Models;
|
||||||
|
using LightTube.Database;
|
||||||
|
|
||||||
|
namespace LightTube.Controllers
|
||||||
|
{
|
||||||
|
public class YoutubeController : Controller
|
||||||
|
{
|
||||||
|
private readonly ILogger<YoutubeController> _logger;
|
||||||
|
private readonly Youtube _youtube;
|
||||||
|
|
||||||
|
public YoutubeController(ILogger<YoutubeController> logger, Youtube youtube)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_youtube = youtube;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("/watch")]
|
||||||
|
public async Task<IActionResult> Watch(string v, string quality = null)
|
||||||
|
{
|
||||||
|
Task[] tasks = {
|
||||||
|
_youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||||
|
_youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||||
|
ReturnYouTubeDislike.GetDislikes(v)
|
||||||
|
};
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
bool cookieCompatibility = false;
|
||||||
|
if (Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||||
|
bool.TryParse(compatibilityString, out cookieCompatibility);
|
||||||
|
|
||||||
|
PlayerContext context = new()
|
||||||
|
{
|
||||||
|
Player = (tasks[0] as Task<YoutubePlayer>)?.Result,
|
||||||
|
Video = (tasks[1] as Task<YoutubeVideo>)?.Result,
|
||||||
|
Engagement = (tasks[2] as Task<YoutubeDislikes>)?.Result,
|
||||||
|
Resolution = quality ?? (tasks[0] as Task<YoutubePlayer>)?.Result.Formats.FirstOrDefault(x => x.FormatId != "17")?.FormatNote,
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request),
|
||||||
|
CompatibilityMode = cookieCompatibility
|
||||||
|
};
|
||||||
|
return View(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("/download")]
|
||||||
|
public async Task<IActionResult> Download(string v)
|
||||||
|
{
|
||||||
|
Task[] tasks = {
|
||||||
|
_youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||||
|
_youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||||
|
ReturnYouTubeDislike.GetDislikes(v)
|
||||||
|
};
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
bool cookieCompatibility = false;
|
||||||
|
if (Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||||
|
bool.TryParse(compatibilityString, out cookieCompatibility);
|
||||||
|
|
||||||
|
PlayerContext context = new()
|
||||||
|
{
|
||||||
|
Player = (tasks[0] as Task<YoutubePlayer>)?.Result,
|
||||||
|
Video = (tasks[1] as Task<YoutubeVideo>)?.Result,
|
||||||
|
Engagement = null,
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request),
|
||||||
|
CompatibilityMode = cookieCompatibility
|
||||||
|
};
|
||||||
|
return View(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("/embed/{v}")]
|
||||||
|
public async Task<IActionResult> Embed(string v, string quality = null, bool compatibility = false)
|
||||||
|
{
|
||||||
|
Task[] tasks = {
|
||||||
|
_youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||||
|
_youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||||
|
ReturnYouTubeDislike.GetDislikes(v)
|
||||||
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
|
||||||
|
bool cookieCompatibility = false;
|
||||||
|
if (Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||||
|
bool.TryParse(compatibilityString, out cookieCompatibility);
|
||||||
|
|
||||||
|
PlayerContext context = new()
|
||||||
|
{
|
||||||
|
Player = (tasks[0] as Task<YoutubePlayer>)?.Result,
|
||||||
|
Video = (tasks[1] as Task<YoutubeVideo>)?.Result,
|
||||||
|
Engagement = (tasks[2] as Task<YoutubeDislikes>)?.Result,
|
||||||
|
Resolution = quality ?? (tasks[0] as Task<YoutubePlayer>)?.Result.Formats.FirstOrDefault(x => x.FormatId != "17")?.FormatNote,
|
||||||
|
CompatibilityMode = compatibility || cookieCompatibility,
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
};
|
||||||
|
return View(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("/results")]
|
||||||
|
public async Task<IActionResult> Search(string search_query, string continuation = null)
|
||||||
|
{
|
||||||
|
SearchContext context = new()
|
||||||
|
{
|
||||||
|
Query = search_query,
|
||||||
|
ContinuationKey = continuation,
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
};
|
||||||
|
if (!string.IsNullOrWhiteSpace(search_query))
|
||||||
|
{
|
||||||
|
context.Results = await _youtube.SearchAsync(search_query, continuation, HttpContext.GetLanguage(),
|
||||||
|
HttpContext.GetRegion());
|
||||||
|
Response.Cookies.Append("search_query", search_query);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Results =
|
||||||
|
new YoutubeSearchResults
|
||||||
|
{
|
||||||
|
Refinements = Array.Empty<string>(),
|
||||||
|
EstimatedResults = 0,
|
||||||
|
Results = Array.Empty<DynamicItem>(),
|
||||||
|
ContinuationKey = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return View(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("/playlist")]
|
||||||
|
public async Task<IActionResult> Playlist(string list, string continuation = null, int? delete = null, string add = null, string remove = null)
|
||||||
|
{
|
||||||
|
HttpContext.TryGetUser(out LTUser user, "web");
|
||||||
|
|
||||||
|
YoutubePlaylist pl = list.StartsWith("LT-PL")
|
||||||
|
? await (await DatabaseManager.Playlists.GetPlaylist(list)).ToYoutubePlaylist()
|
||||||
|
: await _youtube.GetPlaylistAsync(list, continuation, HttpContext.GetLanguage(), HttpContext.GetRegion());
|
||||||
|
|
||||||
|
string message = "";
|
||||||
|
|
||||||
|
if (list.StartsWith("LT-PL") && (await DatabaseManager.Playlists.GetPlaylist(list)).Visibility == PlaylistVisibility.PRIVATE && pl.Channel.Name != user?.UserID)
|
||||||
|
pl = new YoutubePlaylist
|
||||||
|
{
|
||||||
|
Id = null,
|
||||||
|
Title = "",
|
||||||
|
Description = "",
|
||||||
|
VideoCount = "",
|
||||||
|
ViewCount = "",
|
||||||
|
LastUpdated = "",
|
||||||
|
Thumbnail = Array.Empty<Thumbnail>(),
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = "",
|
||||||
|
Id = "",
|
||||||
|
SubscriberCount = "",
|
||||||
|
Avatars = Array.Empty<Thumbnail>()
|
||||||
|
},
|
||||||
|
Videos = Array.Empty<DynamicItem>(),
|
||||||
|
ContinuationKey = null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(pl.Title)) message = "Playlist unavailable";
|
||||||
|
|
||||||
|
if (list.StartsWith("LT-PL") && pl.Channel.Name == user?.UserID)
|
||||||
|
{
|
||||||
|
if (delete != null)
|
||||||
|
{
|
||||||
|
LTVideo removed = await DatabaseManager.Playlists.RemoveVideoFromPlaylist(list, delete.Value);
|
||||||
|
message += $"Removed video '{removed.Title}'";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (add != null)
|
||||||
|
{
|
||||||
|
LTVideo added = await DatabaseManager.Playlists.AddVideoToPlaylist(list, add);
|
||||||
|
message += $"Added video '{added.Title}'";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(remove))
|
||||||
|
{
|
||||||
|
await DatabaseManager.Playlists.DeletePlaylist(list);
|
||||||
|
message = "Playlist deleted";
|
||||||
|
}
|
||||||
|
|
||||||
|
pl = await (await DatabaseManager.Playlists.GetPlaylist(list)).ToYoutubePlaylist();
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaylistContext context = new()
|
||||||
|
{
|
||||||
|
Playlist = pl,
|
||||||
|
Id = list,
|
||||||
|
ContinuationToken = continuation,
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request),
|
||||||
|
Message = message,
|
||||||
|
Editable = list.StartsWith("LT-PL") && pl.Channel.Name == user?.UserID
|
||||||
|
};
|
||||||
|
return View(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("/channel/{id}")]
|
||||||
|
public async Task<IActionResult> Channel(string id, string continuation = null)
|
||||||
|
{
|
||||||
|
ChannelContext context = new()
|
||||||
|
{
|
||||||
|
Channel = await _youtube.GetChannelAsync(id, ChannelTabs.Videos, continuation, HttpContext.GetLanguage(), HttpContext.GetRegion()),
|
||||||
|
Id = id,
|
||||||
|
ContinuationToken = continuation,
|
||||||
|
MobileLayout = Utils.IsClientMobile(Request)
|
||||||
|
};
|
||||||
|
await DatabaseManager.Channels.UpdateChannel(context.Channel.Id, context.Channel.Name, context.Channel.Subscribers,
|
||||||
|
context.Channel.Avatars.First().Url.ToString());
|
||||||
|
return View(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("/shorts/{id}")]
|
||||||
|
public IActionResult Shorts(string id)
|
||||||
|
{
|
||||||
|
// yea no fuck shorts
|
||||||
|
return Redirect("/watch?v=" + id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
core/LightTube/Database/ChannelManager.cs
Normal file
46
core/LightTube/Database/ChannelManager.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
|
||||||
|
namespace LightTube.Database
|
||||||
|
{
|
||||||
|
public class ChannelManager
|
||||||
|
{
|
||||||
|
private static IMongoCollection<LTChannel> _channelCacheCollection;
|
||||||
|
|
||||||
|
public ChannelManager(IMongoCollection<LTChannel> channelCacheCollection)
|
||||||
|
{
|
||||||
|
_channelCacheCollection = channelCacheCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LTChannel GetChannel(string id)
|
||||||
|
{
|
||||||
|
LTChannel res = _channelCacheCollection.FindSync(x => x.ChannelId == id).FirstOrDefault();
|
||||||
|
return res ?? new LTChannel
|
||||||
|
{
|
||||||
|
Name = "Unknown Channel",
|
||||||
|
ChannelId = id,
|
||||||
|
IconUrl = "",
|
||||||
|
Subscribers = ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LTChannel> UpdateChannel(string id, string name, string subscribers, string iconUrl)
|
||||||
|
{
|
||||||
|
LTChannel channel = new()
|
||||||
|
{
|
||||||
|
ChannelId = id,
|
||||||
|
Name = name,
|
||||||
|
Subscribers = subscribers,
|
||||||
|
IconUrl = iconUrl
|
||||||
|
};
|
||||||
|
if (channel.IconUrl is null && !string.IsNullOrWhiteSpace(GetChannel(id).IconUrl))
|
||||||
|
channel.IconUrl = GetChannel(id).IconUrl;
|
||||||
|
if (await _channelCacheCollection.CountDocumentsAsync(x => x.ChannelId == id) > 0)
|
||||||
|
await _channelCacheCollection.ReplaceOneAsync(x => x.ChannelId == id, channel);
|
||||||
|
else
|
||||||
|
await _channelCacheCollection.InsertOneAsync(channel);
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
154
core/LightTube/Database/DatabaseManager.cs
Normal file
154
core/LightTube/Database/DatabaseManager.cs
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using InnerTube;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace LightTube.Database
|
||||||
|
{
|
||||||
|
public static class DatabaseManager
|
||||||
|
{
|
||||||
|
public static readonly string ApiUaRegex = "LightTubeApiClient\\/([0-9.]*) ([\\S]+?)\\/([0-9.]*) \\(([\\s\\S]+?)\\)";
|
||||||
|
|
||||||
|
private static IMongoCollection<LTUser> _userCollection;
|
||||||
|
private static IMongoCollection<LTLogin> _tokenCollection;
|
||||||
|
private static IMongoCollection<LTChannel> _channelCacheCollection;
|
||||||
|
private static IMongoCollection<LTPlaylist> _playlistCollection;
|
||||||
|
private static IMongoCollection<LTVideo> _videoCacheCollection;
|
||||||
|
public static LoginManager Logins { get; private set; }
|
||||||
|
public static ChannelManager Channels { get; private set; }
|
||||||
|
public static PlaylistManager Playlists { get; private set; }
|
||||||
|
|
||||||
|
public static void Init(string connstr, Youtube youtube)
|
||||||
|
{
|
||||||
|
MongoClient client = new(connstr);
|
||||||
|
IMongoDatabase database = client.GetDatabase("lighttube");
|
||||||
|
_userCollection = database.GetCollection<LTUser>("users");
|
||||||
|
_tokenCollection = database.GetCollection<LTLogin>("tokens");
|
||||||
|
_playlistCollection = database.GetCollection<LTPlaylist>("playlists");
|
||||||
|
_channelCacheCollection = database.GetCollection<LTChannel>("channelCache");
|
||||||
|
_videoCacheCollection = database.GetCollection<LTVideo>("videoCache");
|
||||||
|
Logins = new LoginManager(_userCollection, _tokenCollection);
|
||||||
|
Channels = new ChannelManager(_channelCacheCollection);
|
||||||
|
Playlists = new PlaylistManager(_userCollection, _playlistCollection, _videoCacheCollection, youtube);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CreateLocalAccount(this HttpContext context)
|
||||||
|
{
|
||||||
|
bool accountExists = false;
|
||||||
|
|
||||||
|
// Check local account
|
||||||
|
if (context.Request.Cookies.TryGetValue("account_data", out string accountJson))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (accountJson != null)
|
||||||
|
{
|
||||||
|
LTUser tempUser = JsonConvert.DeserializeObject<LTUser>(accountJson) ?? new LTUser();
|
||||||
|
if (tempUser.UserID == "Local Account" && tempUser.PasswordHash == "local_account")
|
||||||
|
accountExists = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account already exists, just leave it there
|
||||||
|
if (accountExists) return;
|
||||||
|
|
||||||
|
LTUser user = new()
|
||||||
|
{
|
||||||
|
UserID = "Local Account",
|
||||||
|
PasswordHash = "local_account",
|
||||||
|
SubscribedChannels = new List<string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Response.Cookies.Append("account_data", JsonConvert.SerializeObject(user), new CookieOptions
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.MaxValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetUser(this HttpContext context, out LTUser user, string requiredScope)
|
||||||
|
{
|
||||||
|
// Check local account
|
||||||
|
if (context.Request.Cookies.TryGetValue("account_data", out string accountJson))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (accountJson != null)
|
||||||
|
{
|
||||||
|
LTUser tempUser = JsonConvert.DeserializeObject<LTUser>(accountJson) ?? new LTUser();
|
||||||
|
if (tempUser.UserID == "Local Account" && tempUser.PasswordHash == "local_account")
|
||||||
|
{
|
||||||
|
user = tempUser;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
user = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cloud account
|
||||||
|
if (!context.Request.Cookies.TryGetValue("token", out string token))
|
||||||
|
if (context.Request.Headers.TryGetValue("Authorization", out StringValues tokens))
|
||||||
|
token = tokens.ToString();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
user = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (token != null)
|
||||||
|
{
|
||||||
|
user = Logins.GetUserFromToken(token).Result;
|
||||||
|
LTLogin login = Logins.GetLoginFromToken(token).Result;
|
||||||
|
if (login.Scopes.Contains(requiredScope))
|
||||||
|
{
|
||||||
|
#pragma warning disable 4014
|
||||||
|
login.UpdateLastAccess(DateTimeOffset.Now);
|
||||||
|
#pragma warning restore 4014
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
user = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
user = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetRssUser(string token, out LTUser user)
|
||||||
|
{
|
||||||
|
if (token is null)
|
||||||
|
{
|
||||||
|
user = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
user = Logins.GetUserFromRssToken(token).Result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
user = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
core/LightTube/Database/LTChannel.cs
Normal file
31
core/LightTube/Database/LTChannel.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System.Xml;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace LightTube.Database
|
||||||
|
{
|
||||||
|
[BsonIgnoreExtraElements]
|
||||||
|
public class LTChannel
|
||||||
|
{
|
||||||
|
public string ChannelId;
|
||||||
|
public string Name;
|
||||||
|
public string Subscribers;
|
||||||
|
public string IconUrl;
|
||||||
|
|
||||||
|
public XmlNode GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("Channel");
|
||||||
|
item.SetAttribute("id", ChannelId);
|
||||||
|
item.SetAttribute("subscribers", Subscribers);
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Name");
|
||||||
|
title.InnerText = Name;
|
||||||
|
item.AppendChild(title);
|
||||||
|
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Avatar");
|
||||||
|
thumbnail.InnerText = IconUrl;
|
||||||
|
item.AppendChild(thumbnail);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
core/LightTube/Database/LTLogin.cs
Normal file
84
core/LightTube/Database/LTLogin.cs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
using System.Xml;
|
||||||
|
using Humanizer;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
using MyCSharp.HttpUserAgentParser;
|
||||||
|
|
||||||
|
namespace LightTube.Database
|
||||||
|
{
|
||||||
|
[BsonIgnoreExtraElements]
|
||||||
|
public class LTLogin
|
||||||
|
{
|
||||||
|
public string Identifier;
|
||||||
|
public string Email;
|
||||||
|
public string Token;
|
||||||
|
public string UserAgent;
|
||||||
|
public string[] Scopes;
|
||||||
|
public DateTimeOffset Created = DateTimeOffset.MinValue;
|
||||||
|
public DateTimeOffset LastSeen = DateTimeOffset.MinValue;
|
||||||
|
|
||||||
|
public XmlDocument GetXmlElement()
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement login = doc.CreateElement("Login");
|
||||||
|
login.SetAttribute("id", Identifier);
|
||||||
|
login.SetAttribute("user", Email);
|
||||||
|
|
||||||
|
XmlElement token = doc.CreateElement("Token");
|
||||||
|
token.InnerText = Token;
|
||||||
|
login.AppendChild(token);
|
||||||
|
|
||||||
|
XmlElement scopes = doc.CreateElement("Scopes");
|
||||||
|
foreach (string scope in Scopes)
|
||||||
|
{
|
||||||
|
XmlElement scopeElement = doc.CreateElement("Scope");
|
||||||
|
scopeElement.InnerText = scope;
|
||||||
|
login.AppendChild(scopeElement);
|
||||||
|
}
|
||||||
|
login.AppendChild(scopes);
|
||||||
|
|
||||||
|
doc.AppendChild(login);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetTitle()
|
||||||
|
{
|
||||||
|
Match match = Regex.Match(UserAgent, DatabaseManager.ApiUaRegex);
|
||||||
|
if (match.Success)
|
||||||
|
return $"API App: {match.Groups[2]} {match.Groups[3]}";
|
||||||
|
|
||||||
|
HttpUserAgentInformation client = HttpUserAgentParser.Parse(UserAgent);
|
||||||
|
StringBuilder sb = new($"{client.Name} {client.Version}");
|
||||||
|
if (client.Platform.HasValue)
|
||||||
|
sb.Append($" on {client.Platform.Value.PlatformType.ToString()}");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetDescription()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new();
|
||||||
|
sb.AppendLine($"Created: {Created.Humanize(DateTimeOffset.Now)}");
|
||||||
|
sb.AppendLine($"Last seen: {LastSeen.Humanize(DateTimeOffset.Now)}");
|
||||||
|
|
||||||
|
Match match = Regex.Match(UserAgent, DatabaseManager.ApiUaRegex);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"API version: {HttpUtility.HtmlEncode(match.Groups[1])}");
|
||||||
|
sb.AppendLine($"App info: {HttpUtility.HtmlEncode(match.Groups[4])}");
|
||||||
|
sb.AppendLine("Allowed scopes:");
|
||||||
|
foreach (string scope in Scopes) sb.AppendLine($"- {scope}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateLastAccess(DateTimeOffset newTime)
|
||||||
|
{
|
||||||
|
await DatabaseManager.Logins.UpdateLastAccess(Identifier, newTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
core/LightTube/Database/LTPlaylist.cs
Normal file
68
core/LightTube/Database/LTPlaylist.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using InnerTube.Models;
|
||||||
|
|
||||||
|
namespace LightTube.Database
|
||||||
|
{
|
||||||
|
public class LTPlaylist
|
||||||
|
{
|
||||||
|
public string Id;
|
||||||
|
public string Name;
|
||||||
|
public string Description;
|
||||||
|
public PlaylistVisibility Visibility;
|
||||||
|
public List<string> VideoIds;
|
||||||
|
public string Author;
|
||||||
|
public DateTimeOffset LastUpdated;
|
||||||
|
|
||||||
|
public async Task<YoutubePlaylist> ToYoutubePlaylist()
|
||||||
|
{
|
||||||
|
List<Thumbnail> t = new();
|
||||||
|
if (VideoIds.Count > 0)
|
||||||
|
t.Add(new Thumbnail { Url = $"https://i.ytimg.com/vi_webp/{VideoIds.First()}/maxresdefault.webp" });
|
||||||
|
YoutubePlaylist playlist = new()
|
||||||
|
{
|
||||||
|
Id = Id,
|
||||||
|
Title = Name,
|
||||||
|
Description = Description,
|
||||||
|
VideoCount = VideoIds.Count.ToString(),
|
||||||
|
ViewCount = "0",
|
||||||
|
LastUpdated = "Last updated " + LastUpdated.ToString("MMMM dd, yyyy"),
|
||||||
|
Thumbnail = t.ToArray(),
|
||||||
|
Channel = new Channel
|
||||||
|
{
|
||||||
|
Name = Author,
|
||||||
|
Id = GenerateChannelId(),
|
||||||
|
SubscriberCount = "0 subscribers",
|
||||||
|
Avatars = Array.Empty<Thumbnail>()
|
||||||
|
},
|
||||||
|
Videos = (await DatabaseManager.Playlists.GetPlaylistVideos(Id)).Select(x =>
|
||||||
|
{
|
||||||
|
x.Index = VideoIds.IndexOf(x.Id) + 1;
|
||||||
|
return x;
|
||||||
|
}).Cast<DynamicItem>().ToArray(),
|
||||||
|
ContinuationKey = null
|
||||||
|
};
|
||||||
|
return playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateChannelId()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new("LTU-" + Author.Trim() + "_");
|
||||||
|
|
||||||
|
string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||||
|
Random rng = new(Author.GetHashCode());
|
||||||
|
while (sb.Length < 32) sb.Append(alphabet[rng.Next(0, alphabet.Length)]);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PlaylistVisibility
|
||||||
|
{
|
||||||
|
PRIVATE,
|
||||||
|
UNLISTED,
|
||||||
|
VISIBLE
|
||||||
|
}
|
||||||
|
}
|
102
core/LightTube/Database/LTUser.cs
Normal file
102
core/LightTube/Database/LTUser.cs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace LightTube.Database
|
||||||
|
{
|
||||||
|
[BsonIgnoreExtraElements]
|
||||||
|
public class LTUser
|
||||||
|
{
|
||||||
|
public string UserID;
|
||||||
|
public string PasswordHash;
|
||||||
|
public List<string> SubscribedChannels;
|
||||||
|
public bool ApiAccess;
|
||||||
|
public string RssToken;
|
||||||
|
|
||||||
|
public async Task<string> GenerateRssFeed(string hostUrl, int limit)
|
||||||
|
{
|
||||||
|
XmlDocument document = new();
|
||||||
|
XmlElement rss = document.CreateElement("rss");
|
||||||
|
rss.SetAttribute("version", "2.0");
|
||||||
|
|
||||||
|
XmlElement channel = document.CreateElement("channel");
|
||||||
|
|
||||||
|
XmlElement title = document.CreateElement("title");
|
||||||
|
title.InnerText = "LightTube subscriptions RSS feed for " + UserID;
|
||||||
|
channel.AppendChild(title);
|
||||||
|
|
||||||
|
XmlElement description = document.CreateElement("description");
|
||||||
|
description.InnerText = $"LightTube subscriptions RSS feed for {UserID} with {SubscribedChannels.Count} channels";
|
||||||
|
channel.AppendChild(description);
|
||||||
|
|
||||||
|
FeedVideo[] feeds = await YoutubeRSS.GetMultipleFeeds(SubscribedChannels);
|
||||||
|
IEnumerable<FeedVideo> feedVideos = feeds.Take(limit);
|
||||||
|
|
||||||
|
foreach (FeedVideo video in feedVideos)
|
||||||
|
{
|
||||||
|
XmlElement item = document.CreateElement("item");
|
||||||
|
|
||||||
|
XmlElement id = document.CreateElement("id");
|
||||||
|
id.InnerText = $"id:video:{video.Id}";
|
||||||
|
item.AppendChild(id);
|
||||||
|
|
||||||
|
XmlElement vtitle = document.CreateElement("title");
|
||||||
|
vtitle.InnerText = video.Title;
|
||||||
|
item.AppendChild(vtitle);
|
||||||
|
|
||||||
|
XmlElement vdescription = document.CreateElement("description");
|
||||||
|
vdescription.InnerText = video.Description;
|
||||||
|
item.AppendChild(vdescription);
|
||||||
|
|
||||||
|
XmlElement link = document.CreateElement("link");
|
||||||
|
link.InnerText = $"https://{hostUrl}/watch?v={video.Id}";
|
||||||
|
item.AppendChild(link);
|
||||||
|
|
||||||
|
XmlElement published = document.CreateElement("pubDate");
|
||||||
|
published.InnerText = video.PublishedDate.ToString("R");
|
||||||
|
item.AppendChild(published);
|
||||||
|
|
||||||
|
XmlElement author = document.CreateElement("author");
|
||||||
|
|
||||||
|
XmlElement name = document.CreateElement("name");
|
||||||
|
name.InnerText = video.ChannelName;
|
||||||
|
author.AppendChild(name);
|
||||||
|
|
||||||
|
XmlElement uri = document.CreateElement("uri");
|
||||||
|
uri.InnerText = $"https://{hostUrl}/channel/{video.ChannelId}";
|
||||||
|
author.AppendChild(uri);
|
||||||
|
|
||||||
|
item.AppendChild(author);
|
||||||
|
/*
|
||||||
|
XmlElement mediaGroup = document.CreateElement("media_group");
|
||||||
|
|
||||||
|
XmlElement mediaTitle = document.CreateElement("media_title");
|
||||||
|
mediaTitle.InnerText = video.Title;
|
||||||
|
mediaGroup.AppendChild(mediaTitle);
|
||||||
|
|
||||||
|
XmlElement mediaThumbnail = document.CreateElement("media_thumbnail");
|
||||||
|
mediaThumbnail.SetAttribute("url", video.Thumbnail);
|
||||||
|
mediaGroup.AppendChild(mediaThumbnail);
|
||||||
|
|
||||||
|
XmlElement mediaContent = document.CreateElement("media_content");
|
||||||
|
mediaContent.SetAttribute("url", $"https://{hostUrl}/embed/{video.Id}");
|
||||||
|
mediaContent.SetAttribute("type", "text/html");
|
||||||
|
mediaGroup.AppendChild(mediaContent);
|
||||||
|
|
||||||
|
item.AppendChild(mediaGroup);
|
||||||
|
*/
|
||||||
|
channel.AppendChild(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
rss.AppendChild(channel);
|
||||||
|
|
||||||
|
document.AppendChild(rss);
|
||||||
|
return document.OuterXml;//.Replace("<media_", "<media:").Replace("</media_", "</media:");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
core/LightTube/Database/LTVideo.cs
Normal file
39
core/LightTube/Database/LTVideo.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Xml;
|
||||||
|
using InnerTube.Models;
|
||||||
|
|
||||||
|
namespace LightTube.Database
|
||||||
|
{
|
||||||
|
public class LTVideo : PlaylistVideoItem
|
||||||
|
{
|
||||||
|
public string UploadedAt;
|
||||||
|
public long Views;
|
||||||
|
|
||||||
|
public override XmlElement GetXmlElement(XmlDocument doc)
|
||||||
|
{
|
||||||
|
XmlElement item = doc.CreateElement("Video");
|
||||||
|
item.SetAttribute("id", Id);
|
||||||
|
item.SetAttribute("duration", Duration);
|
||||||
|
item.SetAttribute("views", Views.ToString());
|
||||||
|
item.SetAttribute("uploadedAt", UploadedAt);
|
||||||
|
item.SetAttribute("index", Index.ToString());
|
||||||
|
|
||||||
|
XmlElement title = doc.CreateElement("Title");
|
||||||
|
title.InnerText = Title;
|
||||||
|
item.AppendChild(title);
|
||||||
|
if (Channel is not null)
|
||||||
|
item.AppendChild(Channel.GetXmlElement(doc));
|
||||||
|
|
||||||
|
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
|
||||||
|
{
|
||||||
|
XmlElement thumbnail = doc.CreateElement("Thumbnail");
|
||||||
|
thumbnail.SetAttribute("width", t.Width.ToString());
|
||||||
|
thumbnail.SetAttribute("height", t.Height.ToString());
|
||||||
|
thumbnail.InnerText = t.Url;
|
||||||
|
item.AppendChild(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
172
core/LightTube/Database/LoginManager.cs
Normal file
172
core/LightTube/Database/LoginManager.cs
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using InnerTube.Models;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
|
||||||
|
namespace LightTube.Database
|
||||||
|
{
|
||||||
|
public class LoginManager
|
||||||
|
{
|
||||||
|
private IMongoCollection<LTUser> _userCollection;
|
||||||
|
private IMongoCollection<LTLogin> _tokenCollection;
|
||||||
|
|
||||||
|
public LoginManager(IMongoCollection<LTUser> userCollection, IMongoCollection<LTLogin> tokenCollection)
|
||||||
|
{
|
||||||
|
_userCollection = userCollection;
|
||||||
|
_tokenCollection = tokenCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LTLogin> CreateToken(string email, string password, string userAgent, string[] scopes)
|
||||||
|
{
|
||||||
|
IAsyncCursor<LTUser> users = await _userCollection.FindAsync(x => x.UserID == email);
|
||||||
|
if (!await users.AnyAsync())
|
||||||
|
throw new UnauthorizedAccessException("Invalid credentials");
|
||||||
|
LTUser user = (await _userCollection.FindAsync(x => x.UserID == email)).First();
|
||||||
|
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
|
||||||
|
throw new UnauthorizedAccessException("Invalid credentials");
|
||||||
|
if (!scopes.Contains("web") && !user.ApiAccess)
|
||||||
|
throw new InvalidOperationException("This user has API access disabled");
|
||||||
|
|
||||||
|
LTLogin login = new()
|
||||||
|
{
|
||||||
|
Identifier = Guid.NewGuid().ToString(),
|
||||||
|
Email = email,
|
||||||
|
Token = GenerateToken(256),
|
||||||
|
UserAgent = userAgent,
|
||||||
|
Scopes = scopes.ToArray(),
|
||||||
|
Created = DateTimeOffset.Now,
|
||||||
|
LastSeen = DateTimeOffset.Now
|
||||||
|
};
|
||||||
|
await _tokenCollection.InsertOneAsync(login);
|
||||||
|
return login;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateLastAccess(string id, DateTimeOffset offset)
|
||||||
|
{
|
||||||
|
LTLogin login = (await _tokenCollection.FindAsync(x => x.Identifier == id)).First();
|
||||||
|
login.LastSeen = offset;
|
||||||
|
await _tokenCollection.ReplaceOneAsync(x => x.Identifier == id, login);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveToken(string token)
|
||||||
|
{
|
||||||
|
await _tokenCollection.FindOneAndDeleteAsync(t => t.Token == token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveToken(string email, string password, string identifier)
|
||||||
|
{
|
||||||
|
IAsyncCursor<LTUser> users = await _userCollection.FindAsync(x => x.UserID == email);
|
||||||
|
if (!await users.AnyAsync())
|
||||||
|
throw new KeyNotFoundException("Invalid credentials");
|
||||||
|
LTUser user = (await _userCollection.FindAsync(x => x.UserID == email)).First();
|
||||||
|
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
|
||||||
|
throw new UnauthorizedAccessException("Invalid credentials");
|
||||||
|
|
||||||
|
await _tokenCollection.FindOneAndDeleteAsync(t => t.Identifier == identifier && t.Email == user.UserID);
|
||||||
|
}
|
||||||
|
|
||||||
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
public async Task RemoveTokenFromId(string sourceToken, string identifier)
|
||||||
|
{
|
||||||
|
LTLogin login = (await _tokenCollection.FindAsync(x => x.Token == sourceToken)).First();
|
||||||
|
LTLogin deletedLogin = (await _tokenCollection.FindAsync(x => x.Identifier == identifier)).First();
|
||||||
|
|
||||||
|
if (login.Email == deletedLogin.Email)
|
||||||
|
await _tokenCollection.FindOneAndDeleteAsync(t => t.Identifier == identifier);
|
||||||
|
else
|
||||||
|
throw new UnauthorizedAccessException(
|
||||||
|
"Logged in user does not match the token that is supposed to be deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LTUser> GetUserFromToken(string token)
|
||||||
|
{
|
||||||
|
string email = (await _tokenCollection.FindAsync(x => x.Token == token)).First().Email;
|
||||||
|
return (await _userCollection.FindAsync(u => u.UserID == email)).First();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LTUser> GetUserFromRssToken(string token) => (await _userCollection.FindAsync(u => u.RssToken == token)).First();
|
||||||
|
|
||||||
|
public async Task<LTLogin> GetLoginFromToken(string token)
|
||||||
|
{
|
||||||
|
var res = await _tokenCollection.FindAsync(x => x.Token == token);
|
||||||
|
return res.First();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<LTLogin>> GetAllUserTokens(string token)
|
||||||
|
{
|
||||||
|
string email = (await _tokenCollection.FindAsync(x => x.Token == token)).First().Email;
|
||||||
|
return await (await _tokenCollection.FindAsync(u => u.Email == email)).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetCurrentLoginId(string token)
|
||||||
|
{
|
||||||
|
return (await _tokenCollection.FindAsync(t => t.Token == token)).First().Identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(LTChannel channel, bool subscribed)> SubscribeToChannel(LTUser user, YoutubeChannel channel)
|
||||||
|
{
|
||||||
|
LTChannel ltChannel = await DatabaseManager.Channels.UpdateChannel(channel.Id, channel.Name, channel.Subscribers,
|
||||||
|
channel.Avatars.FirstOrDefault()?.Url);
|
||||||
|
|
||||||
|
if (user.SubscribedChannels.Contains(ltChannel.ChannelId))
|
||||||
|
user.SubscribedChannels.Remove(ltChannel.ChannelId);
|
||||||
|
else
|
||||||
|
user.SubscribedChannels.Add(ltChannel.ChannelId);
|
||||||
|
|
||||||
|
await _userCollection.ReplaceOneAsync(x => x.UserID == user.UserID, user);
|
||||||
|
return (ltChannel, user.SubscribedChannels.Contains(ltChannel.ChannelId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetApiAccess(LTUser user, bool access)
|
||||||
|
{
|
||||||
|
user.ApiAccess = access;
|
||||||
|
await _userCollection.ReplaceOneAsync(x => x.UserID == user.UserID, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteUser(string email, string password)
|
||||||
|
{
|
||||||
|
IAsyncCursor<LTUser> users = await _userCollection.FindAsync(x => x.UserID == email);
|
||||||
|
if (!await users.AnyAsync())
|
||||||
|
throw new KeyNotFoundException("Invalid credentials");
|
||||||
|
LTUser user = (await _userCollection.FindAsync(x => x.UserID == email)).First();
|
||||||
|
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
|
||||||
|
throw new UnauthorizedAccessException("Invalid credentials");
|
||||||
|
|
||||||
|
await _userCollection.DeleteOneAsync(x => x.UserID == email);
|
||||||
|
await _tokenCollection.DeleteManyAsync(x => x.Email == email);
|
||||||
|
foreach (LTPlaylist pl in await DatabaseManager.Playlists.GetUserPlaylists(email))
|
||||||
|
await DatabaseManager.Playlists.DeletePlaylist(pl.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateUser(string email, string password)
|
||||||
|
{
|
||||||
|
IAsyncCursor<LTUser> users = await _userCollection.FindAsync(x => x.UserID == email);
|
||||||
|
if (await users.AnyAsync())
|
||||||
|
throw new DuplicateNameException("A user with that email already exists");
|
||||||
|
|
||||||
|
LTUser user = new()
|
||||||
|
{
|
||||||
|
UserID = email,
|
||||||
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword(password),
|
||||||
|
SubscribedChannels = new List<string>(),
|
||||||
|
RssToken = GenerateToken(32)
|
||||||
|
};
|
||||||
|
await _userCollection.InsertOneAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateToken(int length)
|
||||||
|
{
|
||||||
|
string tokenAlphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-+*/()[]{}";
|
||||||
|
Random rng = new();
|
||||||
|
StringBuilder sb = new();
|
||||||
|
for (int i = 0; i < length; i++)
|
||||||
|
sb.Append(tokenAlphabet[rng.Next(0, tokenAlphabet.Length)]);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
161
core/LightTube/Database/PlaylistManager.cs
Normal file
161
core/LightTube/Database/PlaylistManager.cs
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using InnerTube;
|
||||||
|
using InnerTube.Models;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace LightTube.Database
|
||||||
|
{
|
||||||
|
public class PlaylistManager
|
||||||
|
{
|
||||||
|
private IMongoCollection<LTUser> _userCollection;
|
||||||
|
private IMongoCollection<LTPlaylist> _playlistCollection;
|
||||||
|
private IMongoCollection<LTVideo> _videoCacheCollection;
|
||||||
|
private Youtube _youtube;
|
||||||
|
|
||||||
|
public PlaylistManager(IMongoCollection<LTUser> userCollection, IMongoCollection<LTPlaylist> playlistCollection,
|
||||||
|
IMongoCollection<LTVideo> videoCacheCollection, Youtube youtube)
|
||||||
|
{
|
||||||
|
_userCollection = userCollection;
|
||||||
|
_playlistCollection = playlistCollection;
|
||||||
|
_videoCacheCollection = videoCacheCollection;
|
||||||
|
_youtube = youtube;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LTPlaylist> CreatePlaylist(LTUser user, string name, string description,
|
||||||
|
PlaylistVisibility visibility, string idPrefix = null)
|
||||||
|
{
|
||||||
|
if (await _userCollection.CountDocumentsAsync(x => x.UserID == user.UserID) == 0)
|
||||||
|
throw new UnauthorizedAccessException("Local accounts cannot create playlists");
|
||||||
|
|
||||||
|
LTPlaylist pl = new()
|
||||||
|
{
|
||||||
|
Id = GenerateAuthorId(idPrefix),
|
||||||
|
Name = name,
|
||||||
|
Description = description,
|
||||||
|
Visibility = visibility,
|
||||||
|
VideoIds = new List<string>(),
|
||||||
|
Author = user.UserID,
|
||||||
|
LastUpdated = DateTimeOffset.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
await _playlistCollection.InsertOneAsync(pl).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return pl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LTPlaylist> GetPlaylist(string id)
|
||||||
|
{
|
||||||
|
IAsyncCursor<LTPlaylist> cursor = await _playlistCollection.FindAsync(x => x.Id == id);
|
||||||
|
return await cursor.FirstOrDefaultAsync() ?? new LTPlaylist
|
||||||
|
{
|
||||||
|
Id = null,
|
||||||
|
Name = "",
|
||||||
|
Description = "",
|
||||||
|
Visibility = PlaylistVisibility.VISIBLE,
|
||||||
|
VideoIds = new List<string>(),
|
||||||
|
Author = "",
|
||||||
|
LastUpdated = DateTimeOffset.MinValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<LTVideo>> GetPlaylistVideos(string id)
|
||||||
|
{
|
||||||
|
LTPlaylist pl = await GetPlaylist(id);
|
||||||
|
List<LTVideo> videos = new();
|
||||||
|
|
||||||
|
foreach (string videoId in pl.VideoIds)
|
||||||
|
{
|
||||||
|
IAsyncCursor<LTVideo> cursor = await _videoCacheCollection.FindAsync(x => x.Id == videoId);
|
||||||
|
videos.Add(await cursor.FirstAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
return videos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LTVideo> AddVideoToPlaylist(string playlistId, string videoId)
|
||||||
|
{
|
||||||
|
LTPlaylist pl = await GetPlaylist(playlistId);
|
||||||
|
YoutubeVideo vid = await _youtube.GetVideoAsync(videoId);
|
||||||
|
JObject ytPlayer = await InnerTube.Utils.GetAuthorizedPlayer(videoId, new HttpClient());
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(vid.Id))
|
||||||
|
throw new KeyNotFoundException($"Couldn't find a video with ID '{videoId}'");
|
||||||
|
|
||||||
|
LTVideo v = new()
|
||||||
|
{
|
||||||
|
Id = vid.Id,
|
||||||
|
Title = vid.Title,
|
||||||
|
Thumbnails = ytPlayer?["videoDetails"]?["thumbnail"]?["thumbnails"]?.ToObject<Thumbnail[]>() ?? new []
|
||||||
|
{
|
||||||
|
new Thumbnail { Url = $"https://i.ytimg.com/vi_webp/{vid.Id}/maxresdefault.webp" }
|
||||||
|
},
|
||||||
|
UploadedAt = vid.UploadDate,
|
||||||
|
Views = long.Parse(vid.Views.Split(" ")[0].Replace(",", "").Replace(".", "")),
|
||||||
|
Channel = vid.Channel,
|
||||||
|
Duration = GetDurationString(ytPlayer?["videoDetails"]?["lengthSeconds"]?.ToObject<long>() ?? 0),
|
||||||
|
Index = pl.VideoIds.Count
|
||||||
|
};
|
||||||
|
pl.VideoIds.Add(vid.Id);
|
||||||
|
|
||||||
|
if (await _videoCacheCollection.CountDocumentsAsync(x => x.Id == vid.Id) == 0)
|
||||||
|
await _videoCacheCollection.InsertOneAsync(v);
|
||||||
|
else
|
||||||
|
await _videoCacheCollection.FindOneAndReplaceAsync(x => x.Id == vid.Id, v);
|
||||||
|
|
||||||
|
UpdateDefinition<LTPlaylist> update = Builders<LTPlaylist>.Update
|
||||||
|
.Push(x => x.VideoIds, vid.Id);
|
||||||
|
_playlistCollection.FindOneAndUpdate(x => x.Id == playlistId, update);
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LTVideo> RemoveVideoFromPlaylist(string playlistId, int videoIndex)
|
||||||
|
{
|
||||||
|
LTPlaylist pl = await GetPlaylist(playlistId);
|
||||||
|
|
||||||
|
IAsyncCursor<LTVideo> cursor = await _videoCacheCollection.FindAsync(x => x.Id == pl.VideoIds[videoIndex]);
|
||||||
|
LTVideo v = await cursor.FirstAsync();
|
||||||
|
pl.VideoIds.RemoveAt(videoIndex);
|
||||||
|
|
||||||
|
await _playlistCollection.FindOneAndReplaceAsync(x => x.Id == playlistId, pl);
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<LTPlaylist>> GetUserPlaylists(string userId)
|
||||||
|
{
|
||||||
|
IAsyncCursor<LTPlaylist> cursor = await _playlistCollection.FindAsync(x => x.Author == userId);
|
||||||
|
|
||||||
|
return cursor.ToEnumerable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetDurationString(long length)
|
||||||
|
{
|
||||||
|
string s = TimeSpan.FromSeconds(length).ToString();
|
||||||
|
while (s.StartsWith("00:") && s.Length > 5) s = s[3..];
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GenerateAuthorId(string prefix)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new(string.IsNullOrWhiteSpace(prefix) || prefix.Trim().Length > 20
|
||||||
|
? "LT-PL"
|
||||||
|
: "LT-PL-" + prefix.Trim() + "_");
|
||||||
|
|
||||||
|
string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||||
|
Random rng = new();
|
||||||
|
while (sb.Length < 32) sb.Append(alphabet[rng.Next(0, alphabet.Length)]);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeletePlaylist(string playlistId)
|
||||||
|
{
|
||||||
|
await _playlistCollection.DeleteOneAsync(x => x.Id == playlistId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
core/LightTube/Database/SubscriptionChannels.cs
Normal file
18
core/LightTube/Database/SubscriptionChannels.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Xml;
|
||||||
|
|
||||||
|
namespace LightTube.Database
|
||||||
|
{
|
||||||
|
public class SubscriptionChannels
|
||||||
|
{
|
||||||
|
public LTChannel[] Channels { get; set; }
|
||||||
|
|
||||||
|
public XmlNode GetXmlDocument()
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement feed = doc.CreateElement("Subscriptions");
|
||||||
|
foreach (LTChannel channel in Channels) feed.AppendChild(channel.GetXmlElement(doc));
|
||||||
|
doc.AppendChild(feed);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
core/LightTube/Database/SubscriptionFeed.cs
Normal file
18
core/LightTube/Database/SubscriptionFeed.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Xml;
|
||||||
|
|
||||||
|
namespace LightTube.Database
|
||||||
|
{
|
||||||
|
public class SubscriptionFeed
|
||||||
|
{
|
||||||
|
public FeedVideo[] videos;
|
||||||
|
|
||||||
|
public XmlDocument GetXmlDocument()
|
||||||
|
{
|
||||||
|
XmlDocument doc = new();
|
||||||
|
XmlElement feed = doc.CreateElement("Feed");
|
||||||
|
foreach (FeedVideo feedVideo in videos) feed.AppendChild(feedVideo.GetXmlElement(doc));
|
||||||
|
doc.AppendChild(feed);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
core/LightTube/LightTube.csproj
Normal file
19
core/LightTube/LightTube.csproj
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\InnerTube\InnerTube.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.2" />
|
||||||
|
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||||
|
<PackageReference Include="MongoDB.Driver" Version="2.14.1" />
|
||||||
|
<PackageReference Include="MyCSharp.HttpUserAgentParser" Version="1.1.5" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
11
core/LightTube/Models/ErrorViewModel.cs
Normal file
11
core/LightTube/Models/ErrorViewModel.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LightTube.Models
|
||||||
|
{
|
||||||
|
public class ErrorViewModel
|
||||||
|
{
|
||||||
|
public string RequestId { get; set; }
|
||||||
|
|
||||||
|
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
}
|
||||||
|
}
|
26
core/LightTube/Program.cs
Normal file
26
core/LightTube/Program.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace LightTube
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Configuration.LoadConfiguration();
|
||||||
|
InnerTube.Utils.SetAuthorization(Configuration.Instance.Credentials.CanUseAuthorizedEndpoints(),
|
||||||
|
Configuration.Instance.Credentials.Sapisid, Configuration.Instance.Credentials.Psid);
|
||||||
|
CreateHostBuilder(args).Build().Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||||
|
Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
|
||||||
|
}
|
||||||
|
}
|
60
core/LightTube/Views/Account/Account.cshtml
Normal file
60
core/LightTube/Views/Account/Account.cshtml
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
@using LightTube.Database
|
||||||
|
@using System.Web
|
||||||
|
@model LightTube.Contexts.BaseContext
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Title = "Account";
|
||||||
|
Layout = "_Layout";
|
||||||
|
|
||||||
|
Context.Request.Cookies.TryGetValue("theme", out string theme);
|
||||||
|
if (!new[] { "light", "dark" }.Contains(theme)) theme = "light";
|
||||||
|
|
||||||
|
string newTheme = theme switch {
|
||||||
|
"light" => "dark",
|
||||||
|
"dark" => "light",
|
||||||
|
var _ => "dark"
|
||||||
|
};
|
||||||
|
|
||||||
|
bool compatibility = false;
|
||||||
|
if (Context.Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||||
|
bool.TryParse(compatibilityString, out compatibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="login-container">
|
||||||
|
<div>
|
||||||
|
<div class="fullscreen-account-menu">
|
||||||
|
<h1>Settings</h1>
|
||||||
|
<br>
|
||||||
|
<div class="guide-item">
|
||||||
|
<a href="/toggles/theme?redirectUrl=@(HttpUtility.UrlEncode($"{Context.Request.Path}{Context.Request.QueryString}"))">Switch to @(newTheme) theme</a>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
@if (Context.TryGetUser(out LTUser user, "web"))
|
||||||
|
{
|
||||||
|
<div class="guide-item">
|
||||||
|
<a href="/Account/Settings">Settings</a>
|
||||||
|
</div>
|
||||||
|
@if (user.PasswordHash != "local_account")
|
||||||
|
{
|
||||||
|
<div class="guide-item">
|
||||||
|
<a href="/Account/Logins">Active logins</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="guide-item">
|
||||||
|
<a href="/Account/Logout">Log out</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="guide-item">
|
||||||
|
<a href="/Account/Login">Log in</a>
|
||||||
|
</div>
|
||||||
|
<div class="guide-item">
|
||||||
|
<a href="/Account/Register">Register</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
</div>
|
52
core/LightTube/Views/Account/AddVideoToPlaylist.cshtml
Normal file
52
core/LightTube/Views/Account/AddVideoToPlaylist.cshtml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
@using System.Web
|
||||||
|
@using LightTube.Database
|
||||||
|
@model LightTube.Contexts.AddToPlaylistContext
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Metadata = new Dictionary<string, string>();
|
||||||
|
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
|
||||||
|
ViewBag.Metadata["og:title"] = Model.Video.Title;
|
||||||
|
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||||
|
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Thumbnail)}";
|
||||||
|
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Thumbnail)}";
|
||||||
|
ViewBag.Title = Model.Video.Title;
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="playlist-page">
|
||||||
|
<div class="playlist-info">
|
||||||
|
<div class="thumbnail" style="background-image: url('@Model.Thumbnail')">
|
||||||
|
<a href="/watch?v=@Model.Video.Id">Watch</a>
|
||||||
|
</div>
|
||||||
|
<p class="title">@Model.Video.Title</p>
|
||||||
|
<span class="info">@Model.Video.Views • @Model.Video.UploadDate</span>
|
||||||
|
<div class="channel-info">
|
||||||
|
<a href="/channel/@Model.Video.Channel.Id" class="avatar">
|
||||||
|
<img src="@Model.Video.Channel.Avatars.LastOrDefault()?.Url">
|
||||||
|
</a>
|
||||||
|
<div class="name">
|
||||||
|
<a class="name" href="/channel/@Model.Video.Channel.Id">@Model.Video.Channel.Name</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="video-list playlist-list playlist-video-list">
|
||||||
|
<h3>Add to one of these playlists:</h3>
|
||||||
|
<a class="login-button" href="/Account/CreatePlaylist" style="margin:unset;">Create playlist</a>
|
||||||
|
@foreach (LTPlaylist playlist in Model.Playlists)
|
||||||
|
{
|
||||||
|
<div class="playlist-video">
|
||||||
|
<a href="/playlist?list=@playlist.Id&add=@Model.Id" class="thumbnail"
|
||||||
|
style="background-image: url('https://i.ytimg.com/vi_webp/@playlist.VideoIds.FirstOrDefault()/maxresdefault.webp')">
|
||||||
|
</a>
|
||||||
|
<div class="info">
|
||||||
|
<a href="/playlist?list=@playlist.Id&add=@Model.Id" class="title max-lines-2">
|
||||||
|
@playlist.Name
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<span>@playlist.VideoIds.Count videos</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
25
core/LightTube/Views/Account/CreatePlaylist.cshtml
Normal file
25
core/LightTube/Views/Account/CreatePlaylist.cshtml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@model LightTube.Contexts.BaseContext
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Title = "Create Playlist";
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="login-container">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<form asp-action="CreatePlaylist" method="POST" class="playlist-form">
|
||||||
|
<h1>Create Playlist</h1>
|
||||||
|
<input name="name" type="text" placeholder="Playlist Name">
|
||||||
|
<input name="description" type="text" placeholder="Description">
|
||||||
|
<select name="visibility">
|
||||||
|
<option value="UNLISTED">Anyone with the link can view</option>
|
||||||
|
<option value="PRIVATE">Only you can view</option>
|
||||||
|
</select>
|
||||||
|
<input type="submit" value="Create">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
</div>
|
46
core/LightTube/Views/Account/Delete.cshtml
Normal file
46
core/LightTube/Views/Account/Delete.cshtml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
@using LightTube.Database
|
||||||
|
@model LightTube.Contexts.MessageContext
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Delete Account";
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
||||||
|
{
|
||||||
|
<div class="login-message">
|
||||||
|
@Model.Message
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="login-container">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
@if (Context.Request.Cookies.TryGetValue("account_data", out string _))
|
||||||
|
{
|
||||||
|
Context.TryGetUser(out LTUser user, "web");
|
||||||
|
<form asp-action="Delete" method="POST" class="login-form">
|
||||||
|
<h1>Delete Account</h1>
|
||||||
|
<p>Deleting a local account</p>
|
||||||
|
<input name="email" type="hidden" value="@user.UserID">
|
||||||
|
<input name="password" type="hidden" value="@user.PasswordHash">
|
||||||
|
<input type="submit" value="Delete Account" class="login-button danger">
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form asp-action="Delete" method="POST" class="login-form">
|
||||||
|
<h1>Delete Account</h1>
|
||||||
|
<input name="userid" type="text" placeholder="UserID">
|
||||||
|
<input name="password" type="password" placeholder="Password">
|
||||||
|
<input type="submit" value="Delete Account" class="login-button danger">
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1>Warning!</h1>
|
||||||
|
<p>You cannot undo this operation! After you enter your username and password, your account will get deleted forever.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
29
core/LightTube/Views/Account/Login.cshtml
Normal file
29
core/LightTube/Views/Account/Login.cshtml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
@model LightTube.Contexts.MessageContext
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Login";
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
||||||
|
{
|
||||||
|
<div class="login-message">
|
||||||
|
@Model.Message
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="login-container">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<form asp-action="Login" method="POST" class="login-form">
|
||||||
|
<h1>Log in</h1>
|
||||||
|
<input name="userid" type="text" placeholder="UserID">
|
||||||
|
<input name="password" type="password" placeholder="Password">
|
||||||
|
<input type="submit" value="Login">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Don't have an account?</h2>
|
||||||
|
<a href="/Account/Register" class="login-button">Create an account</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
18
core/LightTube/Views/Account/Logins.cshtml
Normal file
18
core/LightTube/Views/Account/Logins.cshtml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
@using LightTube.Database
|
||||||
|
@model LightTube.Contexts.LoginsContext
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Active Logins";
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1 style="text-align:center;">Active Logins</h1>
|
||||||
|
<div class="logins-container">
|
||||||
|
@foreach (LTLogin login in Model.Logins)
|
||||||
|
{
|
||||||
|
<div class="login">
|
||||||
|
<h2 class="max-lines-1">@(login.Identifier == Model.CurrentLogin ? "(This window) " : "")@login.GetTitle()</h2>
|
||||||
|
<p>@Html.Raw(login.GetDescription().Replace("\n", "<br>"))</p>
|
||||||
|
<a href="/Account/DisableLogin?id=@login.Identifier" class="login-button" style="color:red;">Disable</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
42
core/LightTube/Views/Account/Register.cshtml
Normal file
42
core/LightTube/Views/Account/Register.cshtml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
@model LightTube.Contexts.MessageContext
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Register";
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
||||||
|
{
|
||||||
|
<div class="login-message">
|
||||||
|
@Model.Message
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="login-container">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<form asp-action="Register" method="POST" class="login-form">
|
||||||
|
<h1>Register</h1>
|
||||||
|
<input name="userid" type="text" placeholder="UserID">
|
||||||
|
<input name="password" type="password" placeholder="Password">
|
||||||
|
<input type="submit" value="Register">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1>...or register with a local account</h1>
|
||||||
|
<h2>What is the difference?</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Remote account data is saved in this lighttube instance, while local account data is stored in
|
||||||
|
your browser's cookies
|
||||||
|
<ul>
|
||||||
|
<li>This means that the author of this lighttube instance cannot see your account data</li>
|
||||||
|
<li>It also means that, if you clear your cookies a lot, your account data will also get
|
||||||
|
lost with the cookies</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<a href="/Account/RegisterLocal" class="login-button">Create local account</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
63
core/LightTube/Views/Account/Settings.cshtml
Normal file
63
core/LightTube/Views/Account/Settings.cshtml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
@model LightTube.Contexts.SettingsContext
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Title = "Settings";
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
<div class="settings-content">
|
||||||
|
<h1 style="text-align:center">Settings</h1>
|
||||||
|
<div>
|
||||||
|
<label for="settings-theme">Theme</label>
|
||||||
|
<select id="settings-theme" name="theme">
|
||||||
|
@Html.Raw($"<option value='light' {(Model.Theme == "light" ? "selected" : "")}>Light</option>")
|
||||||
|
@Html.Raw($"<option value='dark' {(Model.Theme == "dark" ? "selected" : "")}>Dark</option>")
|
||||||
|
</select>
|
||||||
|
<p>This is the visual theme the website will use.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="settings-yhl">Content Language</label>
|
||||||
|
<select id="settings-yhl" name="hl">
|
||||||
|
@foreach (KeyValuePair<string, string> o in Model.Languages)
|
||||||
|
{
|
||||||
|
@Html.Raw($"<option value='{o.Key}' {(o.Key == Model.CurrentLanguage ? "selected" : "")}>{o.Value}</option>")
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<p>The language YouTube will deliver the content in. This will not affect LightTube's UI language.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="settings-ygl">Content Region</label>
|
||||||
|
<select id="settings-ygl" name="gl">
|
||||||
|
@foreach (KeyValuePair<string, string> o in Model.Regions)
|
||||||
|
{
|
||||||
|
@Html.Raw($"<option value='{o.Key}' {(o.Key == Model.CurrentRegion ? "selected" : "")}>{o.Value}</option>")
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<p>The language YouTube will deliver the content for. It is used for the explore page and the recommendations.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="settings-player">Player</label>
|
||||||
|
<select id="settings-player" name="compatibility">
|
||||||
|
@Html.Raw($"<option value=\"false\" {(Model.CompatibilityMode ? "" : "selected")}>DASH playback with muxed fallback (recommended)</option>")
|
||||||
|
@Html.Raw($"<option value=\"true\" {(Model.CompatibilityMode ? "selected" : "")}>Muxed formats only (only supports 360p & 720p)</option>")
|
||||||
|
</select>
|
||||||
|
<p>Player behaviour. DASH playback allows for resolutions over 720p, but it is not compatible in all browsers. (e.g: Firefox Mobile)</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="settings-api">API Access</label>
|
||||||
|
<select id="settings-api" name="api-access">
|
||||||
|
@Html.Raw($"<option value=\"true\" {(Model.ApiAccess ? "selected" : "")}>Enabled</option>")
|
||||||
|
@Html.Raw($"<option value=\"false\" {(Model.ApiAccess ? "" : "selected")}>Disabled</option>")
|
||||||
|
</select>
|
||||||
|
<p>This will allow apps to log in using your username and password</p>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-direction:row">
|
||||||
|
<a href="/Account/Logins" class="login-button">Active Logins</a>
|
||||||
|
<a href="/Account/Delete" class="login-button" style="color:red">Delete Account</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<input type="submit" class="login-button" value="Save"/>
|
||||||
|
</form>
|
26
core/LightTube/Views/Feed/Channels.cshtml
Normal file
26
core/LightTube/Views/Feed/Channels.cshtml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
@using LightTube.Database
|
||||||
|
@model LightTube.Contexts.FeedContext
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Title = "Channel list";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="video-list">
|
||||||
|
@foreach (LTChannel channel in Model.Channels)
|
||||||
|
{
|
||||||
|
<div class="channel">
|
||||||
|
<a href="/channel/@channel.ChannelId" class="avatar">
|
||||||
|
<img src="@channel.IconUrl" alt="Channel Avatar">
|
||||||
|
</a>
|
||||||
|
<a href="/channel/@channel.ChannelId" class="info">
|
||||||
|
<span class="name max-lines-2">@channel.Name</span>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<span>@channel.Subscribers</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<button class="subscribe-button" data-cid="@channel.ChannelId">Subscribe</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
8
core/LightTube/Views/Feed/Explore.cshtml
Normal file
8
core/LightTube/Views/Feed/Explore.cshtml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Explore";
|
||||||
|
ViewData["SelectedGuideItem"] = "explore";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div style="text-align: center">
|
||||||
|
<h1>Coming soon!</h1>
|
||||||
|
</div>
|
35
core/LightTube/Views/Feed/Playlists.cshtml
Normal file
35
core/LightTube/Views/Feed/Playlists.cshtml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
@using LightTube.Database
|
||||||
|
@model LightTube.Contexts.PlaylistsContext
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Playlists";
|
||||||
|
ViewData["SelectedGuideItem"] = "library";
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="video-list">
|
||||||
|
<h2>Playlists</h2>
|
||||||
|
@foreach (LTPlaylist playlist in Model.Playlists)
|
||||||
|
{
|
||||||
|
<div class="playlist">
|
||||||
|
<a href="/watch?v=@playlist.VideoIds.FirstOrDefault()&list=@playlist.Id" class="thumbnail" style="background-image: url('https://i.ytimg.com/vi_webp/@playlist.VideoIds.FirstOrDefault()/maxresdefault.webp')">
|
||||||
|
<div>
|
||||||
|
<span>@playlist.VideoIds.Count</span><span>VIDEOS</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="info">
|
||||||
|
<a href="/watch?v=@playlist.VideoIds.FirstOrDefault()&list=@playlist.Id" class="title max-lines-2">@playlist.Name</a>
|
||||||
|
<div>
|
||||||
|
<a href="/channel/@PlaylistManager.GenerateAuthorId(playlist.Author)">@playlist.Author</a>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="/playlist?list=@playlist.Id">
|
||||||
|
<b>View Full Playlist</b>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
55
core/LightTube/Views/Feed/Subscriptions.cshtml
Normal file
55
core/LightTube/Views/Feed/Subscriptions.cshtml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
@using Humanizer
|
||||||
|
@using LightTube.Database
|
||||||
|
@using System.Web
|
||||||
|
@model LightTube.Contexts.FeedContext
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Subscriptions";
|
||||||
|
ViewData["SelectedGuideItem"] = "subs";
|
||||||
|
|
||||||
|
bool minMode = false;
|
||||||
|
if (Context.Request.Cookies.TryGetValue("minMode", out string minModeString))
|
||||||
|
bool.TryParse(minModeString, out minMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="horizontal-channel-list" style="max-width: @(!Model.MobileLayout ? $"calc(100vw - {(minMode ? 80 : 312)}px);" : "")">
|
||||||
|
<a href="/feed/channels" class="channel">
|
||||||
|
<i class="bi bi-gear"></i>
|
||||||
|
<div class="name max-lines-2">Manage Channels</div>
|
||||||
|
</a>
|
||||||
|
<a href="/rss?token=@HttpUtility.UrlEncode(Model.RssToken)" class="channel">
|
||||||
|
<i class="bi bi-rss"></i>
|
||||||
|
<div class="name max-lines-2">RSS Feed</div>
|
||||||
|
</a>
|
||||||
|
@foreach (LTChannel channel in Model.Channels)
|
||||||
|
{
|
||||||
|
<a href="/channel/@channel.ChannelId" class="channel">
|
||||||
|
<img src="@channel.IconUrl" loading="lazy">
|
||||||
|
<div class="name max-lines-2">@channel.Name</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rich-video-grid">
|
||||||
|
@foreach (FeedVideo video in Model.Videos)
|
||||||
|
{
|
||||||
|
<div class="video">
|
||||||
|
<a href="/watch?v=@video.Id" class="thumbnail img-thumbnail">
|
||||||
|
<img src="@video.Thumbnail" loading="lazy">
|
||||||
|
</a>
|
||||||
|
<a href="/channel/@video.ChannelId" class="avatar">
|
||||||
|
<img src="@Model.Channels.First(x => x.ChannelId == video.ChannelId).IconUrl">
|
||||||
|
</a>
|
||||||
|
<div class="info">
|
||||||
|
<a href="/watch?v=@video.Id" class="title max-lines-2">@video.Title</a>
|
||||||
|
<div>
|
||||||
|
<a href="/channel/@video.ChannelId">@video.ChannelName</a>
|
||||||
|
<div>
|
||||||
|
<span>@video.ViewCount views</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>@video.PublishedDate.Humanize(DateTimeOffset.Now)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
15
core/LightTube/Views/Home/Index.cshtml
Normal file
15
core/LightTube/Views/Home/Index.cshtml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
@model LightTube.Contexts.BaseContext
|
||||||
|
@{
|
||||||
|
ViewBag.Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["og:title"] = "LightTube",
|
||||||
|
["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}",
|
||||||
|
["og:description"] = "An alternative, privacy respecting front end for YouTube",
|
||||||
|
};
|
||||||
|
ViewData["Title"] = "Home Page";
|
||||||
|
ViewData["SelectedGuideItem"] = "home";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div style="text-align: center">
|
||||||
|
<h1>@Configuration.Instance.Interface.MessageOfTheDay</h1>
|
||||||
|
</div>
|
17
core/LightTube/Views/Shared/Error.cshtml
Normal file
17
core/LightTube/Views/Shared/Error.cshtml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@model LightTube.Contexts.ErrorContext
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Error";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
<p>
|
||||||
|
You can try other alternatives to access this resource such as:
|
||||||
|
<b>
|
||||||
|
<a href="https://invidio.us@($"{Model.Path}{Context.Request.QueryString}")">Invidious</a>
|
||||||
|
</b>
|
||||||
|
or
|
||||||
|
<b>
|
||||||
|
<a href="https://youtube.com@($"{Model.Path}{Context.Request.QueryString}")">YouTube</a>
|
||||||
|
</b>
|
||||||
|
</p>
|
139
core/LightTube/Views/Shared/_Layout.cshtml
Normal file
139
core/LightTube/Views/Shared/_Layout.cshtml
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
@using System.Web
|
||||||
|
@using LightTube.Contexts
|
||||||
|
@model LightTube.Contexts.BaseContext
|
||||||
|
@{
|
||||||
|
bool compatibility = false;
|
||||||
|
if (Context.Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
|
||||||
|
bool.TryParse(compatibilityString, out compatibility);
|
||||||
|
|
||||||
|
bool minMode = false;
|
||||||
|
if (Context.Request.Cookies.TryGetValue("minMode", out string minModeString))
|
||||||
|
bool.TryParse(minModeString, out minMode);
|
||||||
|
}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<meta property="og:site_name" content="lighttube" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
@if (ViewBag.Metadata is not null)
|
||||||
|
{
|
||||||
|
@foreach (KeyValuePair<string, string> metaTag in ViewBag.Metadata)
|
||||||
|
{
|
||||||
|
if (metaTag.Key.StartsWith("og:"))
|
||||||
|
{
|
||||||
|
<meta property="@metaTag.Key" content="@metaTag.Value"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<meta name="@metaTag.Key" content="@metaTag.Value"/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<meta property="theme-color" content="#AA0000" />
|
||||||
|
<title>@ViewData["Title"] - lighttube</title>
|
||||||
|
@if ((ViewData["HideGuide"] ?? false).Equals(true))
|
||||||
|
{
|
||||||
|
<style> .guide { display: none !important; } </style>
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Context.Request.Cookies.TryGetValue("theme", out string theme);
|
||||||
|
if (!new[] { "light", "dark" }.Contains(theme)) theme = "light";
|
||||||
|
}
|
||||||
|
<link rel="stylesheet" href="@($"~/css/colors-{theme}.css")" asp-append-version="true"/>
|
||||||
|
@if (Model.MobileLayout)
|
||||||
|
{
|
||||||
|
<link rel="stylesheet" href="~/css/mobile.css" asp-append-version="true"/>
|
||||||
|
<link rel="stylesheet" href="~/css/lt-video/player-mobile.css" asp-append-version="true"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<link rel="stylesheet" href="~/css/desktop.css" asp-append-version="true"/>
|
||||||
|
<link rel="stylesheet" href="~/css/lt-video/player-desktop.css" asp-append-version="true"/>
|
||||||
|
}
|
||||||
|
<link rel="stylesheet" href="~/css/bootstrap-icons/bootstrap-icons.css" asp-append-version="true"/>
|
||||||
|
<link rel="icon" href="~/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="top-bar @(ViewData["UseFullSizeSearchBar"]?.Equals(true) ?? false ? "full-size-search" : "")">
|
||||||
|
<a class="logo" href="/">light<b>tube</b></a>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<form action="/results">
|
||||||
|
<input type="text" placeholder="Search" name="search_query" value="@(Model is SearchContext ctx ? ctx.Query : Context.Request.Cookies.TryGetValue("search_query", out string s) ? s : "")">
|
||||||
|
<input type="submit" value="Search">
|
||||||
|
</form>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="search-button">
|
||||||
|
<a class="icon-link" href="/results">
|
||||||
|
<i class="bi bi-search"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="account" tabindex="-1">
|
||||||
|
<a class="icon-link" href="/Account">
|
||||||
|
<i class="bi bi-person-circle"></i>
|
||||||
|
</a>
|
||||||
|
<div class="account-menu">
|
||||||
|
@Html.Partial("_LoginLogoutPartial")
|
||||||
|
<div class="guide-item"><a href="/toggles/theme?redirectUrl=@(HttpUtility.UrlEncode($"{Context.Request.Path}{Context.Request.QueryString}"))">Toggle Theme</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guide @(minMode ? "minmode" : "")">
|
||||||
|
<div class="guide-item @(ViewData["SelectedGuideItem"] as string == "home" ? "active" : "")">
|
||||||
|
<a href="/">
|
||||||
|
<i class="icon bi bi-house-door"></i>
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="guide-item @(ViewData["SelectedGuideItem"] as string == "explore" ? "active" : "")">
|
||||||
|
<a href="/feed/explore">
|
||||||
|
<i class="icon bi bi-compass"></i>
|
||||||
|
Explore
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="guide-item @(ViewData["SelectedGuideItem"] as string == "subs" ? "active" : "")">
|
||||||
|
<a href="/feed/subscriptions">
|
||||||
|
<i class="icon bi bi-inboxes"></i>
|
||||||
|
Subscriptions
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="guide-item @(ViewData["SelectedGuideItem"] as string == "library" ? "active" : "")">
|
||||||
|
<a href="/feed/library">
|
||||||
|
<i class="icon bi bi-list-ul"></i>
|
||||||
|
Library
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="hide-on-minmode guide-item">
|
||||||
|
<a href="/toggles/collapse_guide?redirectUrl=@(HttpUtility.UrlEncode($"{Context.Request.Path}{Context.Request.QueryString}"))">
|
||||||
|
<i class="icon"><i class="bi bi-arrow-left-square"></i></i>
|
||||||
|
Collapse Guide
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="show-on-minmode guide-item">
|
||||||
|
<a href="/toggles/collapse_guide?redirectUrl=@(HttpUtility.UrlEncode($"{Context.Request.Path}{Context.Request.QueryString}"))">
|
||||||
|
<i class="icon"><i class="bi bi-arrow-right-square"></i></i>
|
||||||
|
Expand
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<hr class="hide-on-minmode">
|
||||||
|
<p class="hide-on-minmode">
|
||||||
|
<a href="https://gitlab.com/kuylar/lighttube/-/blob/master/README.md">About</a><br>
|
||||||
|
<a href="https://gitlab.com/kuylar/lighttube/-/blob/master/OTHERLIBS.md">How LightTube works</a><br>
|
||||||
|
<a href="https://gitlab.com/kuylar/lighttube">Source code</a>
|
||||||
|
<a href="https://gitlab.com/kuylar/lighttube/-/wikis/XML-API">API</a>
|
||||||
|
<a href="https://gitlab.com/kuylar/lighttube/-/blob/master/LICENSE">License</a><br>
|
||||||
|
<span style="font-weight: normal">Running on LightTube v@(Utils.GetVersion())</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app">
|
||||||
|
@RenderBody()
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||||
|
@await RenderSectionAsync("Scripts", required: false)
|
||||||
|
</body>
|
||||||
|
</html>
|
16
core/LightTube/Views/Shared/_LoginLogoutPartial.cshtml
Normal file
16
core/LightTube/Views/Shared/_LoginLogoutPartial.cshtml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
@using LightTube.Database
|
||||||
|
@if (Context.TryGetUser(out LTUser user, "web"))
|
||||||
|
{
|
||||||
|
<div class="guide-item"><a>@user.UserID.Split("@")[0]</a></div>
|
||||||
|
@if (user.PasswordHash != "local_account")
|
||||||
|
{
|
||||||
|
<div class="guide-item"><a href="/Account/Logins">Active logins</a></div>
|
||||||
|
}
|
||||||
|
<div class="guide-item"><a href="/Account/Logout">Log out</a></div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="guide-item"><a href="/Account/Login">Log in</a></div>
|
||||||
|
<div class="guide-item"><a href="/Account/Register">Register</a></div>
|
||||||
|
}
|
||||||
|
<div class="guide-item"><a href="/Account/Settings">Settings</a></div>
|
80
core/LightTube/Views/Youtube/Channel.cshtml
Normal file
80
core/LightTube/Views/Youtube/Channel.cshtml
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
@using InnerTube.Models
|
||||||
|
@using System.Web
|
||||||
|
@model LightTube.Contexts.ChannelContext
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Metadata = new Dictionary<string, string>();
|
||||||
|
ViewBag.Metadata["og:title"] = Model.Channel.Name;
|
||||||
|
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||||
|
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Channel.Avatars.FirstOrDefault()?.Url?.ToString())}";
|
||||||
|
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Channel.Avatars.LastOrDefault()?.Url?.ToString())}";
|
||||||
|
ViewBag.Metadata["og:description"] = Model.Channel.Description;
|
||||||
|
ViewBag.Title = Model.Channel.Name;
|
||||||
|
Layout = "_Layout";
|
||||||
|
|
||||||
|
DynamicItem[] contents;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
contents = ((ItemSectionItem)((ItemSectionItem)Model.Channel.Videos[0]).Contents[0]).Contents;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
contents = Model.Channel.Videos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="channel-page">
|
||||||
|
@if (Model.Channel.Banners.Length > 0)
|
||||||
|
{
|
||||||
|
<img class="channel-banner" alt="Channel Banner" src="@Model.Channel.Banners.Last().Url">
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="channel-info-container">
|
||||||
|
<div class="channel-info">
|
||||||
|
<a href="/channel/@Model.Channel.Id" class="avatar">
|
||||||
|
<img src="@Model.Channel.Avatars.LastOrDefault()?.Url" alt="Channel Avatar">
|
||||||
|
</a>
|
||||||
|
<div class="name">
|
||||||
|
<a>@Model.Channel.Name</a>
|
||||||
|
<span>@Model.Channel.Subscribers</span>
|
||||||
|
</div>
|
||||||
|
<button class="subscribe-button" data-cid="@Model.Channel.Id">Subscribe</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>About</h3>
|
||||||
|
<p>@Html.Raw(Model.Channel.GetHtmlDescription())</p>
|
||||||
|
<br><br>
|
||||||
|
<h3>Uploads</h3>
|
||||||
|
<div class="video-grid">
|
||||||
|
@foreach (VideoItem video in contents.Where(x => x is VideoItem).Cast<VideoItem>())
|
||||||
|
{
|
||||||
|
<a href="/watch?v=@video.Id" class="video">
|
||||||
|
<div class="thumbnail" style="background-image: url('@video.Thumbnails.LastOrDefault()?.Url')"><span class="video-length">@video.Duration</span></div>
|
||||||
|
<div class="info">
|
||||||
|
<span class="title max-lines-2">@video.Title</span>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<span>@video.Views views</span>
|
||||||
|
<span>@video.UploadedAt</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination-buttons">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.ContinuationToken))
|
||||||
|
{
|
||||||
|
<a href="/channel?id=@Model.Id">First Page</a>
|
||||||
|
}
|
||||||
|
<div class="divider"></div>
|
||||||
|
<span>•</span>
|
||||||
|
<div class="divider"></div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(contents.FirstOrDefault(x => x is ContinuationItem)?.Id))
|
||||||
|
{
|
||||||
|
<a href="/channel/@Model.Id?continuation=@(contents.FirstOrDefault(x => x is ContinuationItem)?.Id)">Next Page</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
95
core/LightTube/Views/Youtube/Download.cshtml
Normal file
95
core/LightTube/Views/Youtube/Download.cshtml
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
@using System.Web
|
||||||
|
@using InnerTube
|
||||||
|
@using InnerTube.Models
|
||||||
|
@model LightTube.Contexts.PlayerContext
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Metadata = new Dictionary<string, string>();
|
||||||
|
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
|
||||||
|
ViewBag.Metadata["og:title"] = Model.Player.Title;
|
||||||
|
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||||
|
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.FirstOrDefault()?.Url?.ToString())}";
|
||||||
|
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.LastOrDefault()?.Url?.ToString())}";
|
||||||
|
ViewBag.Title = Model.Player.Title;
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="playlist-page">
|
||||||
|
<div class="playlist-info">
|
||||||
|
<div class="thumbnail" style="background-image: url('@Model.Player.Thumbnails.Last().Url')">
|
||||||
|
<a href="/watch?v=@Model.Player.Id">Watch</a>
|
||||||
|
</div>
|
||||||
|
<p class="title">@Model.Player.Title</p>
|
||||||
|
<span class="info">@Model.Video.Views • @Model.Video.UploadDate</span>
|
||||||
|
<div class="channel-info">
|
||||||
|
<a href="/channel/@Model.Player.Channel.Id" class="avatar">
|
||||||
|
<img src="@Model.Player.Channel.Avatars.LastOrDefault()?.Url">
|
||||||
|
</a>
|
||||||
|
<div class="name">
|
||||||
|
<a class="name" href="/channel/@Model.Player.Channel.Id">@Model.Player.Channel.Name</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="video-list download-list playlist-video-list">
|
||||||
|
<div class="format-list">
|
||||||
|
<h2>Muxed formats</h2>
|
||||||
|
<p>These downloads have both video and audio in them</p>
|
||||||
|
@foreach (Format format in Model.Player.Formats)
|
||||||
|
{
|
||||||
|
<div class="download-format">
|
||||||
|
<div>
|
||||||
|
@format.FormatNote
|
||||||
|
</div>
|
||||||
|
<a href="/proxy/download/@Model.Video.Id/@format.FormatId/@(HttpUtility.UrlEncode(Model.Video.Title)).@format.GetExtension()">
|
||||||
|
<i class="bi bi-download"></i>
|
||||||
|
Download through LightTube
|
||||||
|
</a>
|
||||||
|
<a href="@format.Url">
|
||||||
|
<i class="bi bi-cloud-download"></i>
|
||||||
|
Download through YouTube
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="format-list">
|
||||||
|
<h2>Audio only formats</h2>
|
||||||
|
<p>These downloads have only have audio in them</p>
|
||||||
|
@foreach (Format format in Model.Player.AdaptiveFormats.Where(x => x.VideoCodec == "none"))
|
||||||
|
{
|
||||||
|
<div class="download-format">
|
||||||
|
<div>
|
||||||
|
@format.FormatNote (Codec: @format.AudioCodec, Sample Rate: @format.AudioSampleRate)
|
||||||
|
</div>
|
||||||
|
<a href="/proxy/download/@Model.Video.Id/@format.FormatId/@(HttpUtility.UrlEncode(Model.Video.Title)).@format.GetExtension()">
|
||||||
|
<i class="bi bi-download"></i>
|
||||||
|
Download through LightTube
|
||||||
|
</a>
|
||||||
|
<a href="@format.Url">
|
||||||
|
<i class="bi bi-cloud-download"></i>
|
||||||
|
Download through YouTube
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="format-list">
|
||||||
|
<h2>Video only formats</h2>
|
||||||
|
<p>These downloads have only have video in them</p>
|
||||||
|
@foreach (Format format in Model.Player.AdaptiveFormats.Where(x => x.AudioCodec == "none"))
|
||||||
|
{
|
||||||
|
<div class="download-format">
|
||||||
|
<div>
|
||||||
|
@format.FormatNote (Codec: @format.VideoCodec)
|
||||||
|
</div>
|
||||||
|
<a href="/proxy/download/@Model.Video.Id/@format.FormatId/@(HttpUtility.UrlEncode(Model.Video.Title)).@format.GetExtension()">
|
||||||
|
<i class="bi bi-download"></i>
|
||||||
|
Download through LightTube
|
||||||
|
</a>
|
||||||
|
<a href="@format.Url">
|
||||||
|
<i class="bi bi-cloud-download"></i>
|
||||||
|
Download through YouTube
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
146
core/LightTube/Views/Youtube/Embed.cshtml
Normal file
146
core/LightTube/Views/Youtube/Embed.cshtml
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
@using System.Collections.Specialized
|
||||||
|
@using System.Web
|
||||||
|
@using InnerTube.Models
|
||||||
|
@model LightTube.Contexts.PlayerContext
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Metadata = new Dictionary<string, string>();
|
||||||
|
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
|
||||||
|
ViewBag.Metadata["og:title"] = Model.Player.Title;
|
||||||
|
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||||
|
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.FirstOrDefault()?.Url?.ToString())}";
|
||||||
|
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.LastOrDefault()?.Url?.ToString())}";
|
||||||
|
ViewBag.Metadata["og:description"] = Model.Player.Description;
|
||||||
|
ViewBag.Title = Model.Player.Title;
|
||||||
|
|
||||||
|
Layout = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ViewBag.Metadata["og:video"] = $"/proxy/video?url={HttpUtility.UrlEncode(Model.Player.Formats.First().Url.ToString())}";
|
||||||
|
Model.Resolution ??= Model.Player.Formats.First().FormatNote;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
bool live = Model.Player.Formats.Length == 0 && Model.Player.AdaptiveFormats.Length > 0;
|
||||||
|
bool canPlay = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<meta property="og:site_name" content="lighttube"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
@if (ViewBag.Metadata is not null)
|
||||||
|
{
|
||||||
|
@foreach (KeyValuePair<string, string> metaTag in ViewBag.Metadata)
|
||||||
|
{
|
||||||
|
if (metaTag.Key.StartsWith("og:"))
|
||||||
|
{
|
||||||
|
<meta property="@metaTag.Key" content="@metaTag.Value"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<meta name="@metaTag.Key" content="@metaTag.Value"/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<meta property="theme-color" content="#AA0000"/>
|
||||||
|
<title>@ViewData["Title"] - lighttube</title>
|
||||||
|
<link rel="stylesheet" href="~/css/bootstrap-icons/bootstrap-icons.css"/>
|
||||||
|
<link rel="stylesheet" href="~/css/desktop.css" asp-append-version="true"/>
|
||||||
|
<link rel="stylesheet" href="~/css/lt-video/player-desktop.css" asp-append-version="true"/>
|
||||||
|
<link rel="icon" href="~/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
@if (live)
|
||||||
|
{
|
||||||
|
<video class="player" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
|
||||||
|
</video>
|
||||||
|
}
|
||||||
|
else if (Model.Player.Formats.Length > 0)
|
||||||
|
{
|
||||||
|
<video class="player" controls src="/proxy/media/@Model.Player.Id/@HttpUtility.UrlEncode(Model.Player.Formats.First(x => x.FormatNote == Model.Resolution && x.FormatId != "17").FormatId)" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
|
||||||
|
@foreach (Subtitle subtitle in Model.Player.Subtitles ?? Array.Empty<Subtitle>())
|
||||||
|
{
|
||||||
|
@:<track src="/proxy/caption/@Model.Player.Id/@HttpUtility.UrlEncode(subtitle.Language).Replace("+", "%20")" label="@subtitle.Language" kind="subtitles">
|
||||||
|
}
|
||||||
|
</video>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
canPlay = false;
|
||||||
|
<div id="player" class="player error" style="background-image: url('@Model.Player.Thumbnails.LastOrDefault()?.Url')">
|
||||||
|
@if (string.IsNullOrWhiteSpace(Model.Player.ErrorMessage))
|
||||||
|
{
|
||||||
|
<span>
|
||||||
|
No playable streams returned from the API (@Model.Player.Formats.Length/@Model.Player.AdaptiveFormats.Length)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>
|
||||||
|
@Model.Player.ErrorMessage
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (canPlay)
|
||||||
|
{
|
||||||
|
<script src="/js/lt-video/player-desktop.js"></script>
|
||||||
|
@if (!Model.CompatibilityMode && !live)
|
||||||
|
{
|
||||||
|
<script src="/js/shaka-player/shaka-player.compiled.min.js"></script>
|
||||||
|
<script>
|
||||||
|
let player = undefined;
|
||||||
|
loadPlayerWithShaka("video", {
|
||||||
|
"id": "@Model.Video.Id",
|
||||||
|
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||||
|
"embed": true,
|
||||||
|
"live": false,
|
||||||
|
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
|
||||||
|
}, [
|
||||||
|
@foreach(Format f in Model.Player.Formats.Reverse())
|
||||||
|
{
|
||||||
|
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/video?url=@HttpUtility.UrlEncode(f.Url)"},
|
||||||
|
}
|
||||||
|
], "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).mpd").then(x => player = x).catch(alert);;
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
else if (live)
|
||||||
|
{
|
||||||
|
<script src="/js/hls.js/hls.min.js"></script>
|
||||||
|
<script>
|
||||||
|
let player = undefined;
|
||||||
|
loadPlayerWithHls("video", {
|
||||||
|
"id": "@(Model.Video.Id)",
|
||||||
|
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||||
|
"embed": true,
|
||||||
|
"live": true
|
||||||
|
}, "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).m3u8").then(x => player = x).catch(alert);
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<script>
|
||||||
|
const player = new Player("video", {
|
||||||
|
"id": "@Model.Video.Id",
|
||||||
|
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
|
||||||
|
"embed": true,
|
||||||
|
"live": false,
|
||||||
|
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
|
||||||
|
}, [
|
||||||
|
@foreach(Format f in Model.Player.Formats.Reverse())
|
||||||
|
{
|
||||||
|
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/video?url=@HttpUtility.UrlEncode(f.Url)"},
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</body>
|
||||||
|
</html>
|
85
core/LightTube/Views/Youtube/Playlist.cshtml
Normal file
85
core/LightTube/Views/Youtube/Playlist.cshtml
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
@using InnerTube.Models
|
||||||
|
@using System.Web
|
||||||
|
@model LightTube.Contexts.PlaylistContext
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Model.Playlist.Title;
|
||||||
|
ViewBag.Metadata = new Dictionary<string, string>();
|
||||||
|
ViewBag.Metadata["og:title"] = Model.Playlist.Title;
|
||||||
|
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
|
||||||
|
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Playlist.Thumbnail.FirstOrDefault()?.Url?.ToString())}";
|
||||||
|
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Playlist.Thumbnail.LastOrDefault()?.Url?.ToString())}";
|
||||||
|
ViewBag.Metadata["og:description"] = Model.Playlist.Description;
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
||||||
|
{
|
||||||
|
<div class="playlist-message" style="padding: 16px;background-color: var(--border-color); color: var(--text-primary);">
|
||||||
|
@Model.Message
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="playlist-page">
|
||||||
|
<div class="playlist-info">
|
||||||
|
<div class="thumbnail" style="background-image: url('@Model.Playlist.Thumbnail.LastOrDefault()?.Url')">
|
||||||
|
<a href="/watch?v=@Model.Playlist.Videos.FirstOrDefault()?.Id&list=@Model.Id">Play all</a>
|
||||||
|
</div>
|
||||||
|
<p class="title">@Model.Playlist.Title</p>
|
||||||
|
<span class="info">@Model.Playlist.VideoCount videos • @Model.Playlist.ViewCount views • @Model.Playlist.LastUpdated</span>
|
||||||
|
<span class="description">@Html.Raw(Model.Playlist.GetHtmlDescription())</span>
|
||||||
|
<a href="/playlist?list=@Model.Id&remove=true" class="login-button" style="margin:unset;">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
Delete playlist
|
||||||
|
</a>
|
||||||
|
<div class="channel-info">
|
||||||
|
<a href="/channel/@Model.Playlist.Channel.Id" class="avatar">
|
||||||
|
<img src="@Model.Playlist.Channel.Avatars.LastOrDefault()?.Url">
|
||||||
|
</a>
|
||||||
|
<div class="name">
|
||||||
|
<a class="name" href="/channel/@Model.Playlist.Channel.Id">@Model.Playlist.Channel.Name</a>
|
||||||
|
</div>
|
||||||
|
<button class="subscribe-button" data-cid="@Model.Playlist.Channel.Id">Subscribe</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="video-list playlist-video-list">
|
||||||
|
@foreach (PlaylistVideoItem video in Model.Playlist.Videos.Cast<PlaylistVideoItem>())
|
||||||
|
{
|
||||||
|
<div class="playlist-video">
|
||||||
|
<a href="/watch?v=@video.Id&list=@Model.Id" class="index">
|
||||||
|
@video.Index
|
||||||
|
</a>
|
||||||
|
<a href="/watch?v=@video.Id&list=@Model.Id" class="thumbnail"
|
||||||
|
style="background-image: url('@video.Thumbnails.LastOrDefault()?.Url')">
|
||||||
|
<span class="video-length">@video.Duration</span>
|
||||||
|
</a>
|
||||||
|
<div class="info">
|
||||||
|
<a href="/watch?v=@video.Id&list=@Model.Id" class="title max-lines-2">
|
||||||
|
@video.Title
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<a href="/channel/@video.Channel.Name">@video.Channel.Name</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (Model.Editable)
|
||||||
|
{
|
||||||
|
<a href="/playlist?list=@Model.Id&delete=@(video.Index - 1)" class="edit">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pagination-buttons">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.ContinuationToken))
|
||||||
|
{
|
||||||
|
<a href="/playlist?list=@Model.Id">First Page</a>
|
||||||
|
}
|
||||||
|
<div class="divider"></div>
|
||||||
|
<span>•</span>
|
||||||
|
<div class="divider"></div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.Playlist.ContinuationKey))
|
||||||
|
{
|
||||||
|
<a href="/playlist?list=@Model.Id&continuation=@Model.Playlist.ContinuationKey">Next Page</a>
|
||||||
|
}
|
||||||
|
</div>
|
28
core/LightTube/Views/Youtube/Search.cshtml
Normal file
28
core/LightTube/Views/Youtube/Search.cshtml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
@using InnerTube.Models
|
||||||
|
@model LightTube.Contexts.SearchContext
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Title = Model.Query;
|
||||||
|
Layout = "_Layout";
|
||||||
|
ViewData["UseFullSizeSearchBar"] = Model.MobileLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="video-list">
|
||||||
|
@foreach (DynamicItem preview in Model.Results.Results)
|
||||||
|
{
|
||||||
|
@preview.GetHtml()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="pagination-buttons">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.ContinuationKey))
|
||||||
|
{
|
||||||
|
<a href="/results?search_query=@Model.Query">First Page</a>
|
||||||
|
}
|
||||||
|
<div class="divider"></div>
|
||||||
|
<span>•</span>
|
||||||
|
<div class="divider"></div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.Results.ContinuationKey))
|
||||||
|
{
|
||||||
|
<a href="/results?search_query=@Model.Query&continuation=@Model.Results.ContinuationKey">Next Page</a>
|
||||||
|
}
|
||||||
|
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user