Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/api-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: API checks

on:
pull_request:
paths:
- "apps/api/**"
- ".github/workflows/api-ci.yml"
push:
branches:
- main
- feat/youtube-first-discovery
paths:
- "apps/api/**"
- ".github/workflows/api-ci.yml"

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/api
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: apps/api/requirements-dev.txt
- name: Install dependencies
run: python -m pip install -r requirements-dev.txt
- name: Ruff (core correctness rules)
run: ruff check app tests --select E4,E7,E9,F
- name: Pytest
run: pytest -q
111 changes: 51 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,107 +1,98 @@
# Musify

Cliente de música com uma arquitetura de providers: catálogo e reprodução Audius, mais áudio local escolhido pelo utilizador.
Cliente de música com providers de descoberta e reprodução: YouTube como fonte prioritária para pesquisa e descoberta, Audius como alternativa e áudio local gerido no browser.

| Camada | Tecnologia |
|---|---|
| Frontend | Next.js 14 (App Router) + TypeScript + Tailwind CSS + Zustand + TanStack Query |
| Backend | FastAPI (Python) + Audius API + YouTube Data API/OAuth (legado durante a migração) |
| Reprodução online | Endpoint oficial de stream do Audius, com verificação de disponibilidade e restrições de acesso |
| Reprodução local | Ficheiros selecionados pelo utilizador e reproduzidos no browser; nunca enviados para o backend |
| Backend | FastAPI (Python) + YouTube Data API/OAuth + Audius API |
| Reprodução YouTube | Player oficial incorporado do YouTube |
| Reprodução Audius | Endpoint oficial de stream Audius, com verificação de disponibilidade e restrições |
| Reprodução local | Ficheiros escolhidos pelo utilizador e reproduzidos no browser; nunca enviados para o backend |
| Cache | Redis com fallback em memória |

## Arquitetura de providers

- **Audius:** pesquisa, tendências, metadados e reprodução através da API oficial. Musify valida se a faixa está disponível, se é streamable e se tem restrições de acesso antes de iniciar a reprodução.
- **Áudio local:** será gerido no frontend através da API de áudio do browser. Os ficheiros permanecem no dispositivo do utilizador.
- **YouTube:** a integração existente permanece isolada durante a migração; não é utilizada para extrair áudio nem para contornar o player oficial.
- **YouTube:** provider prioritário para pesquisa e descoberta. A pesquisa usa a YouTube Data API v3; quando a conta está ligada, as recomendações usam os artistas e vídeos da playlist de gostos como sinais para procurar música relacionada.
- **Audius:** provider de fallback quando o YouTube não devolve resultados ou a API está temporariamente indisponível. A reprodução continua sujeita às verificações de disponibilidade e acesso.
- **Áudio local:** permanece no dispositivo do utilizador e não é enviado para o backend.

A API Audius é de leitura para pesquisa e descoberta; uma API key é opcional e pode aumentar os limites de utilização. Consulta a [documentação oficial](https://docs.audius.co/api/) antes de configurar uma chave.
O Musify não acede ao algoritmo privado de recomendações da página inicial do YouTube. As recomendações personalizadas são uma descoberta própria baseada nos dados autorizados pelo utilizador. O servidor não extrai áudio do YouTube; as faixas YouTube são reproduzidas pelo player oficial incorporado.

## Configuração

Cria `apps/api/.env` com as credenciais do Google OAuth:
Cria o arquivo apps/api/.env com as credenciais:

```env
~~~env
YOUTUBE_API_KEY=
YOUTUBE_REGION_CODE=AO
AUDIUS_API_KEY=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=http://localhost:8000/api/auth/youtube/callback
FRONTEND_URL=http://localhost:3000
REDIS_URL=redis://localhost:6379/0
```
~~~

O backend lê esse arquivo quando iniciado a partir de `apps/api`, e o Docker Compose
também o carrega para o container da API. O `.env.example` não é rastreado pelo Git;
não o uses como arquivo de configuração.
O backend lê esse arquivo quando iniciado a partir de apps/api, e o Docker Compose também o carrega para o container da API. Não coloques segredos no repositório.

### Autorizar o callback OAuth no Google

O Google exige que o callback enviado pela API seja registado exatamente no cliente
OAuth usado pelo backend. Para desenvolvimento local, o valor é:
O callback precisa estar registado exatamente no cliente OAuth usado pelo backend. Para desenvolvimento local:

```text
~~~text
http://localhost:8000/api/auth/youtube/callback
```

1. Abre o [Google Cloud Console](https://console.cloud.google.com/) e seleciona o
projeto correspondente ao `GOOGLE_CLIENT_ID` configurado em `apps/api/.env`.
2. Vai a **APIs e serviços → Credenciais** e abre o ID de cliente OAuth 2.0 usado
pelo backend. O tipo da aplicação deve ser **Aplicativo da Web**.
3. Em **URIs de redirecionamento autorizados**, adiciona exatamente
`http://localhost:8000/api/auth/youtube/callback` e guarda.
Não o adiciones apenas em **Origens JavaScript autorizadas**.
4. Confirma que `GOOGLE_REDIRECT_URI` em `apps/api/.env` contém esse mesmo URI,
sem espaços nem barra final extra. `localhost` e `127.0.0.1`, HTTP e HTTPS,
portas e caminhos diferentes não são equivalentes.
5. Reinicia a API para ela carregar o `.env` atualizado. Se usas Docker Compose,
executa `docker compose up -d --force-recreate api` na raiz do projeto.
6. Abre a aplicação em `http://localhost:3000` e tenta **Conectar com YouTube**
novamente. Se ainda falhar, compara o `redirect_uri` mostrado nos detalhes do
erro Google com a lista de URIs autorizados do mesmo ID de cliente OAuth.

O URI exibido no erro `redirect_uri_mismatch` é o que a API realmente enviou.
Se estiveres a usar outro domínio/porta ou HTTPS, configura o URI público completo
em `GOOGLE_REDIRECT_URI` e regista exatamente o mesmo valor no cliente OAuth.

### Backend

```bash
~~~

1. Abre o Google Cloud Console e seleciona o projeto correspondente ao GOOGLE_CLIENT_ID.
2. Vai a **APIs e serviços → Credenciais** e abre o ID de cliente OAuth 2.0. O tipo deve ser **Aplicativo da Web**.
3. Em **URIs de redirecionamento autorizados**, adiciona exatamente o URI acima.
4. Confirma que GOOGLE_REDIRECT_URI em apps/api/.env contém o mesmo valor.
5. Reinicia a API e tenta **Conectar com YouTube** novamente.

## Executar

Backend:

~~~bash
cd apps/api
python -m venv .venv
# Ativa o ambiente virtual e instala as dependências:
pip install -r requirements-dev.txt
uvicorn app.main:app --reload --port 8000
```
~~~

### Frontend
Frontend:

```bash
~~~bash
cd apps/web
npm install
npm run dev
```
~~~

## Endpoints
## Endpoints principais

| Método | Endpoint | Descrição |
|---|---|---|
| GET | `/api/health` | Estado da API e do Redis |
| GET | `/api/music/search?q=` | Pesquisa Audius |
| GET | `/api/music/trending` | Faixas em tendência no Audius |
| GET | `/api/music/tracks/{id}` | Metadados normalizados de uma faixa |
| GET | `/api/music/tracks/{id}/stream` | Verifica acesso e redireciona para o stream oficial Audius |
| GET | `/api/home` | Conteúdo inicial legado |
| GET | `/api/moods`, `/api/moods/{key}` | Playlists por disposição |
| GET | `/api/playlist/{id}` | Playlist YouTube legado |
| GET | `/api/search?q=` | Pesquisa YouTube legado |
| GET/POST | `/api/auth/youtube/*` | Autenticação YouTube existente |
| GET | /api/health | Estado da API e do Redis |
| GET | /api/music/search?q= | Pesquisa YouTube primeiro; Audius como fallback |
| GET | /api/music/trending | Descoberta regional YouTube; Audius como fallback |
| GET | /api/music/recommendations | Recomendações baseadas nos gostos da conta ligada; público quando não há sessão |
| GET | /api/music/tracks/{id} | Metadados de uma faixa Audius |
| GET | /api/music/tracks/{id}/stream | Verifica acesso e redireciona para o stream oficial Audius |
| GET | /api/home | Conteúdo inicial legado |
| GET | /api/moods, /api/moods/{key} | Playlists por disposição |
| GET | /api/playlist/{id} | Playlist YouTube |
| GET | /api/search?q= | Pesquisa YouTube de catálogo completo |
| GET/POST | /api/auth/youtube/* | Autenticação YouTube |

## Quota e cache

A pesquisa usa search.list e os detalhes são carregados em lote com videos.list. As respostas ficam em cache para reduzir chamadas. Recomendações por conta usam uma chave de cache derivada do ID do canal, evitando servir os gostos de uma conta a outra. O limite diário e o consumo da API devem ser acompanhados no Google Cloud Console.

## Testes

```bash
~~~bash
cd apps/api
pytest -v
ruff check app tests
```
~~~
2 changes: 1 addition & 1 deletion apps/api/app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ async def callback(request: Request, state: str = '', code: str = '', error: str
)
channel_response.raise_for_status()
channels = channel_response.json().get('items', [])
except (httpx.HTTPError, KeyError, ValueError) as exc:
except (httpx.HTTPError, KeyError, ValueError):
return RedirectResponse(f'{frontend}/account?youtube=exchange_error', status_code=303)
if not channels:
return RedirectResponse(f'{frontend}/account?youtube=no_channel', status_code=303)
Expand Down
2 changes: 2 additions & 0 deletions apps/api/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Settings(BaseSettings):
redis_url: str = 'redis://localhost:6379/0'
cors_origins: str = 'http://localhost:3000'
youtube_api_key: str = ''
youtube_region_code: str = 'AO'
audius_api_key: str = ''
google_client_id: str = ''
google_client_secret: str = ''
Expand All @@ -19,6 +20,7 @@ class Settings(BaseSettings):
ttl_moods: int = 86400
ttl_playlist: int = 21600
ttl_search: int = 1800
ttl_recommendations: int = 21600

@property
def cors_list(self) -> list[str]:
Expand Down
1 change: 0 additions & 1 deletion apps/api/app/routers/meta.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import hashlib
import time

import httpx
from fastapi import APIRouter, Cookie, HTTPException, Query, Response
Expand Down
Loading
Loading