Skip to content

Commit

Permalink
⚡️ Big refactoring
Browse files Browse the repository at this point in the history
- switch to rapid-api-client for youtube api/webapi
- rework unit tests
- better async fetch
- rework api/proxy/web routers
- keep only channel_id and @user
  - user names without @ are now deprecated
  • Loading branch information
essembeh committed Oct 9, 2024
1 parent 654a244 commit c18fb40
Show file tree
Hide file tree
Showing 29 changed files with 802 additions and 626 deletions.
57 changes: 46 additions & 11 deletions API.rest
Original file line number Diff line number Diff line change
@@ -1,28 +1,63 @@
@base_url = http://localhost:8000

@channel_id = UCVooVnzQxPSTXTMzSi1s6uw

#########################################
## API
#########################################

###
GET {{base_url}}/u/alice
Authorization: Basic alice foo
GET {{base_url}}/api/version


#########################################
## Youtube proxy
#########################################

###
GET {{base_url}}/proxy/rss/UCVooVnzQxPSTXTMzSi1s6uw

###
GET {{base_url}}/proxy/avatar/UCVooVnzQxPSTXTMzSi1s6uw

###
GET {{base_url}}/proxy/home/UCVooVnzQxPSTXTMzSi1s6uw

###
GET {{base_url}}/proxy/rss/@jonnygiger

###
GET {{base_url}}/proxy/avatar/@jonnygiger

###
GET {{base_url}}/proxy/home/@jonnygiger



#########################################
## Frontend
#########################################

### Default page
GET {{base_url}}/

### Error, wrong password
GET {{base_url}}/u/alice
Authorization: Basic alice bar

###
### Alice's homepage
GET {{base_url}}/u/alice
Authorization: Basic alice foo

### Error, wrong password
GET {{base_url}}/u/bob
Authorization: Basic bob foo

###
### Bob's homepage
GET {{base_url}}/u/bob
Authorization: Basic bob bar

###
### Demo's homepage
GET {{base_url}}/u/demo

###
GET {{base_url}}/api/rss/{{channel_id}}

###
GET {{base_url}}/api/avatar/{{channel_id}}
### Watch single video
GET {{base_url}}/watch?v=q5IMA244HXw
4 changes: 3 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ run:
poetry run -- dotenv run -- fastapi dev --host 0.0.0.0 yourss/main.py

test pytest_args="":
poetry run -- pytest {{pytest_args}} tests/
poetry run -- pytest --cov=yourss {{pytest_args}} tests/
poetry run -- coverage html
xdg-open htmlcov/index.html

release bump="patch":
echo "{{bump}}" | grep -E "^(major|minor|patch)$"
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ $ git clone https://github.com/essembeh/YouRSS
$ cd YouRSS
$ poetry install
$ poetry run -- dotenv run fastapi dev yourss/main.py

# or if you use just
$ just run
```

Then visit [http://localhost:8000/](http://localhost:8000/)
Expand Down Expand Up @@ -103,12 +106,13 @@ To configure users:

# Usage

- you can browse a single channel with: `http://my-yourss-instance/@jonnygiger`
- you can browse multiple channels in a single page: `http://my-yourss-instance/@jonnygiger,@berrics`
- the original *RSS* feed can be access at `http://my-yourss-instance/api/rss/@jonnygiger`
- you will be redirected to the channel avatar with `http://my-yourss-instance/api/avatar/@jonnygiger`
- if you defined somes *users*, for example `demo`, the page can be accessed at `http://my-yourss-instance/u/demo`
- you can browse a single channel with: `http://yourss.local/@jonnygiger`
- you can browse multiple channels in a single page: `http://yourss.local/@jonnygiger,@berrics`
- the original *RSS* feed can be access at `http://yourss.local/api/rss/@jonnygiger`
- you will be redirected to the channel avatar with `http://yourss.local/api/avatar/@jonnygiger`
- if you defined somes *users*, for example `demo`, the page can be accessed at `http://yourss.local/u/demo`

> Note: replace `yourss.local` with the URL of your *YouRSS* instance.
# Extenal links

Expand Down
5 changes: 2 additions & 3 deletions charts/yourss/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ yourss:
# theme: light
# channels:
# # channels can be
# # a channel_id like UCVooVnzQxPSTXTMzSi1s6uw
# # a user_id like DAN1ELmadison
# # a slug like @jonnygiger
# # a channel_id (starting with UC) like UCVooVnzQxPSTXTMzSi1s6uw
# # a user (starting with @) like @jonnygiger
# - "@jonnygiger"
# - "@berrics"
# - name: bob
Expand Down
430 changes: 205 additions & 225 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ readme = "README.md"


[tool.poetry.dependencies]
python = "^3.10"
python = "^3.11"
fastapi = {extras = ["standard"], version = "^0.115.0"}
beautifulsoup4 = "^4.12.2"
jinja2 = "^3.1.2"
Expand All @@ -26,6 +26,7 @@ pydantic = "^2.7.1"
pydantic-yaml = "^1.3.0"
pydantic-settings = "^2.2.1"
pydantic-xml = "^2.11.0"
rapid-api-client = "^0.3.4"


[tool.poetry.group.dev.dependencies]
Expand Down
5 changes: 2 additions & 3 deletions samples/users.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ users:
theme: light
channels:
# channels can be
# a channel_id like UCVooVnzQxPSTXTMzSi1s6uw
# a user_id like DAN1ELmadison
# a slug like @jonnygiger
# a channel_id (starting with UC) like UCVooVnzQxPSTXTMzSi1s6uw
# a user (starting with @) like @jonnygiger
- "@jonnygiger"
- "@berrics"
- name: bob
Expand Down
20 changes: 11 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import pytest_asyncio
from fastapi.testclient import TestClient
import pytest
from httpx import ASGITransport, AsyncClient

from yourss.main import app
from yourss.youtube.client import YoutubeClient


@pytest_asyncio.fixture
async def yt_client():
yield YoutubeClient()
@pytest.fixture
def anyio_backend():
return "asyncio"


@pytest_asyncio.fixture
async def yourss_client():
yield TestClient(app)
@pytest.fixture
async def client():
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test", follow_redirects=False
) as client:
yield client
94 changes: 53 additions & 41 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,58 @@
from .test_youtube import CHANNEL_ID, SLUG, USER
import re

import pytest

def test_version(yourss_client):
resp = yourss_client.get("/api/version")
resp.raise_for_status()


def test_api_avatar_slug(yourss_client):
resp = yourss_client.get(f"/api/avatar/{SLUG}", follow_redirects=False)
assert 300 <= resp.status_code < 400


def test_api_avatar_channel(yourss_client):
resp = yourss_client.get(f"/api/avatar/{CHANNEL_ID}", follow_redirects=False)
assert 300 <= resp.status_code < 400


def test_api_avatar_user(yourss_client):
resp = yourss_client.get(f"/api/avatar/{USER}", follow_redirects=False)
assert 300 <= resp.status_code < 400

from yourss.main import app_name, app_version

def test_api_rss_slug(yourss_client):
resp = yourss_client.get(f"/api/rss/{SLUG}", follow_redirects=False)
assert 300 <= resp.status_code < 400


def test_api_rss_channel(yourss_client):
resp = yourss_client.get(f"/api/rss/{CHANNEL_ID}", follow_redirects=False)
assert 300 <= resp.status_code < 400


def test_api_rss_user(yourss_client):
resp = yourss_client.get(f"/api/rss/{USER}", follow_redirects=False)
assert 300 <= resp.status_code < 400


def test_homepage(yourss_client):
resp = yourss_client.get("/")
assert resp.status_code == 200

@pytest.mark.anyio
async def test_version(client):
resp = await client.get("/api/version")
resp.raise_for_status()

def test_page(yourss_client):
resp = yourss_client.get("/@jonnygiger")
assert resp.status_code == 200
payload = resp.json()
assert payload.get("name") == app_name
assert payload.get("version") == app_version


@pytest.mark.anyio
async def test_proxy_rss(client):
channel = await client.get("/proxy/rss/UCVooVnzQxPSTXTMzSi1s6uw")
user = await client.get("/proxy/rss/@jonnygiger")
playlist = await client.get("/proxy/rss/PLw-vK1_d04zZCal3yMX_T23h5nDJ2toTk")

assert user.status_code == channel.status_code == playlist.status_code == 307
assert (
user.headers["Location"]
== user.headers["Location"]
== "https://www.youtube.com/feeds/videos.xml?channel_id=UCVooVnzQxPSTXTMzSi1s6uw"
)
assert (
playlist.headers["Location"]
== "https://www.youtube.com/feeds/videos.xml?playlist_id=PLw-vK1_d04zZCal3yMX_T23h5nDJ2toTk"
)


@pytest.mark.anyio
async def test_proxy_avatar(client):
channel = await client.get("/proxy/avatar/UCVooVnzQxPSTXTMzSi1s6uw")
user = await client.get("/proxy/avatar/@jonnygiger")

assert user.status_code == channel.status_code == 307
assert user.headers["Location"] == user.headers["Location"]
assert re.fullmatch(
r"^https://yt[0-9]+\.googleusercontent\.com/.*$", user.headers["Location"]
)


@pytest.mark.anyio
async def test_proxy_home(client):
channel = await client.get("/proxy/home/UCVooVnzQxPSTXTMzSi1s6uw")
user = await client.get("/proxy/home/@jonnygiger")

assert user.status_code == channel.status_code == 307
assert (
user.headers["Location"]
== user.headers["Location"]
== "https://www.youtube.com/channel/UCVooVnzQxPSTXTMzSi1s6uw"
)
66 changes: 66 additions & 0 deletions tests/test_web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import pytest
from httpx import BasicAuth

from yourss.youtube.utils import bs_parse


@pytest.mark.anyio
async def test_default(client):
resp = await client.get("/")
assert resp.status_code == 307
assert resp.headers["Location"] == "/@CardMagicByJason"


@pytest.mark.anyio
async def test_watch(client):
resp = await client.get("/watch?v=q5IMA244HXw")
assert resp.status_code == 307
assert (
resp.headers["Location"]
== "https://www.youtube-nocookie.com/embed/q5IMA244HXw?autoplay=1&control=2&rel=0"
)


@pytest.mark.anyio
async def test_homepage(client):
# Alice's password is bar
resp = await client.get("/u/alice")
assert resp.status_code == 401

resp = await client.get("/u/alice", auth=BasicAuth("4l1c3", "password"))
assert resp.status_code == 401

resp = await client.get("/u/alice", auth=BasicAuth("alice", "password"))
assert resp.status_code == 401

resp = await client.get("/u/alice", auth=BasicAuth("alice", "foo"))
assert resp.status_code == 200

# Demo has no password
resp = await client.get("/u/demo")
assert resp.status_code == 200

# Unknown page
resp = await client.get("/u/unknown")
assert resp.status_code == 404


@pytest.mark.anyio
async def test_page_content(client):
names = [
"PLw-vK1_d04zZCal3yMX_T23h5nDJ2toTk", # a playlist
"UCVooVnzQxPSTXTMzSi1s6uw", # a channel
"@CardMagicByJason", # a user
"@UCAAAAAAAAAAAAAAAAAAAAAA", # an unknown user
"UCAAAAAAAAAAAAAAAAAAAAAA", # an invalid channel
"foobar", # an invalid name
]
resp = await client.get("/" + ",".join(names))
assert resp.status_code == 200

soup = bs_parse(resp.text)
assert len(soup.find_all("div", class_="yourss-filterable")) == 45

# when no valid feed given, should return 404
resp = await client.get("/UCAAAAAAAAAAAAAAAAAAAAAA,@UCAAAAAAAAAAAAAAAAAAAAAA")
assert resp.status_code == 404
2 changes: 1 addition & 1 deletion tests/test_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from httpx import Client
from pytest import mark

from yourss.rss import Feed
from yourss.youtube import Feed

SAMPLES_FOLDER = Path(__file__).parent.parent / "samples"
FEEDS_FILE = Path(__file__).parent / "feeds.txt"
Expand Down
Loading

0 comments on commit c18fb40

Please sign in to comment.