Skip to content

Commit fb2ff18

Browse files
author
joaquinito2070
committed
Initial release
0 parents  commit fb2ff18

22 files changed

Lines changed: 1084 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-python@v5
16+
with:
17+
python-version: "3.12"
18+
cache: pip
19+
- run: python -m pip install --upgrade pip
20+
- run: python -m pip install -e ".[dev]"
21+
- run: ruff check .
22+
- run: pytest -q

.github/workflows/crawl.yml

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
name: Crawl and build M3U
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
start:
7+
description: "Primer SID (incluido)"
8+
required: true
9+
default: "0"
10+
end:
11+
description: "Último SID (incluido)"
12+
required: true
13+
default: "9999"
14+
concurrency:
15+
description: "Tareas simultáneas, máximo 2048"
16+
required: true
17+
default: "32"
18+
per_host:
19+
description: "Conexiones simultáneas por host"
20+
required: true
21+
default: "16"
22+
max_rps:
23+
description: "Peticiones iniciadas por segundo"
24+
required: true
25+
default: "8"
26+
acknowledge_load:
27+
description: "Para alta carga escribe I_HAVE_PERMISSION"
28+
required: false
29+
default: ""
30+
publish:
31+
description: "Publicar output/ en la rama"
32+
type: boolean
33+
required: true
34+
default: false
35+
schedule:
36+
- cron: "17 3 * * 1"
37+
timezone: "Europe/Madrid"
38+
39+
permissions:
40+
contents: write
41+
42+
concurrency:
43+
group: samcloud-crawl
44+
cancel-in-progress: false
45+
46+
jobs:
47+
crawl:
48+
runs-on: ubuntu-latest
49+
timeout-minutes: 330
50+
env:
51+
SCHEDULED_START: ${{ vars.SAMCLOUD_START || '0' }}
52+
SCHEDULED_END: ${{ vars.SAMCLOUD_END || '9999' }}
53+
steps:
54+
- uses: actions/checkout@v4
55+
- uses: actions/setup-python@v5
56+
with:
57+
python-version: "3.12"
58+
cache: pip
59+
- run: python -m pip install --upgrade pip
60+
- run: python -m pip install .
61+
62+
- name: Resolve inputs
63+
id: params
64+
shell: bash
65+
run: |
66+
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
67+
echo "start=${{ inputs.start }}" >> "$GITHUB_OUTPUT"
68+
echo "end=${{ inputs.end }}" >> "$GITHUB_OUTPUT"
69+
echo "concurrency=${{ inputs.concurrency }}" >> "$GITHUB_OUTPUT"
70+
echo "per_host=${{ inputs.per_host }}" >> "$GITHUB_OUTPUT"
71+
echo "max_rps=${{ inputs.max_rps }}" >> "$GITHUB_OUTPUT"
72+
echo "ack=${{ inputs.acknowledge_load }}" >> "$GITHUB_OUTPUT"
73+
else
74+
echo "start=$SCHEDULED_START" >> "$GITHUB_OUTPUT"
75+
echo "end=$SCHEDULED_END" >> "$GITHUB_OUTPUT"
76+
echo "concurrency=32" >> "$GITHUB_OUTPUT"
77+
echo "per_host=16" >> "$GITHUB_OUTPUT"
78+
echo "max_rps=8" >> "$GITHUB_OUTPUT"
79+
echo "ack=" >> "$GITHUB_OUTPUT"
80+
fi
81+
82+
- name: Crawl
83+
run: |
84+
samcloud-m3u \
85+
--start "${{ steps.params.outputs.start }}" \
86+
--end "${{ steps.params.outputs.end }}" \
87+
--concurrency "${{ steps.params.outputs.concurrency }}" \
88+
--per-host "${{ steps.params.outputs.per_host }}" \
89+
--max-rps "${{ steps.params.outputs.max_rps }}" \
90+
--acknowledge-load "${{ steps.params.outputs.ack }}" \
91+
--no-progress
92+
93+
- name: Upload generated files
94+
uses: actions/upload-artifact@v4
95+
with:
96+
name: samcloud-m3u-${{ github.run_id }}
97+
path: |
98+
output/
99+
data/stations.sqlite3
100+
data/next_sid.txt
101+
retention-days: 30
102+
103+
- name: Publish output to repository
104+
if: github.event_name == 'workflow_dispatch' && inputs.publish
105+
shell: bash
106+
run: |
107+
git config user.name "github-actions[bot]"
108+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
109+
git add output/ data/next_sid.txt
110+
git diff --cached --quiet && exit 0
111+
git commit -m "Update generated SAM Cloud playlist"
112+
git push

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.venv/
2+
__pycache__/
3+
*.py[cod]
4+
.pytest_cache/
5+
.ruff_cache/
6+
*.egg-info/
7+
dist/
8+
build/
9+
data/*.sqlite3
10+
data/*.sqlite3-shm
11+
data/*.sqlite3-wal

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Joaquín Rufo Gutiérrez
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.PHONY: install test lint crawl
2+
3+
install:
4+
python -m pip install -e ".[dev]"
5+
6+
test:
7+
pytest -q
8+
9+
lint:
10+
ruff check .
11+
12+
crawl:
13+
samcloud-m3u --start 0 --end 9999

README.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# SAM Cloud M3U Crawler
2+
3+
Crawler asíncrono en Python para consultar enlaces de escucha de SAM Broadcaster Cloud, seguir redirecciones, detectar `icy-name` y generar una lista M3U con la **URL final redirigida**.
4+
5+
> [!IMPORTANT]
6+
> Utiliza este proyecto únicamente sobre rangos y servicios para los que tengas autorización. La enumeración masiva de SID o una concurrencia extrema puede incumplir condiciones del proveedor o degradar el servicio. El proyecto usa valores prudentes por defecto y bloquea configuraciones agresivas sin una confirmación explícita.
7+
8+
## Funciones
9+
10+
- Rango SID configurable, incluido `0-999999`.
11+
- Hasta 2048 tareas/conexiones configurables como techo técnico.
12+
- Límites separados de concurrencia total, por host y peticiones por segundo.
13+
- Seguimiento de redirecciones HTTP 301, 302, 303, 307 y 308.
14+
- La M3U contiene la URL final, no el enlace intermedio de la API.
15+
- Detección de `icy-name`, `ice-name`, `x-audiocast-name` y descripción ICY.
16+
- Detección básica de M3U/PLS devueltos como cuerpo y extracción del primer stream.
17+
- Reintentos, timeout, checkpoint, reanudación y base SQLite.
18+
- Exportación M3U y JSONL.
19+
- CI y ejecución manual/programada mediante GitHub Actions.
20+
21+
## Instalación
22+
23+
```bash
24+
python -m venv .venv
25+
source .venv/bin/activate # Windows: .venv\\Scripts\\activate
26+
python -m pip install -e ".[dev]"
27+
```
28+
29+
## Ejemplo prudente
30+
31+
```bash
32+
samcloud-m3u \
33+
--start 0 \
34+
--end 9999 \
35+
--concurrency 32 \
36+
--per-host 16 \
37+
--max-rps 8
38+
```
39+
40+
Archivos generados:
41+
42+
- `output/samcloud.m3u`
43+
- `output/stations.jsonl`
44+
- `data/stations.sqlite3`
45+
- `data/next_sid.txt`
46+
47+
## Rango completo
48+
49+
El rango completo se expresa así, pero debería dividirse en lotes y ejecutarse solo con permiso del proveedor:
50+
51+
```bash
52+
samcloud-m3u --start 0 --end 999999 --resume
53+
```
54+
55+
## Techo de 2048 conexiones
56+
57+
El programa admite `--concurrency 2048`, pero las configuraciones de alta carga están bloqueadas salvo confirmación expresa. Incluso con autorización, empieza con cifras bajas y mide la respuesta del servidor.
58+
59+
```bash
60+
samcloud-m3u \
61+
--start 0 \
62+
--end 999999 \
63+
--concurrency 2048 \
64+
--per-host 2048 \
65+
--max-rps 2048 \
66+
--acknowledge-load I_HAVE_PERMISSION
67+
```
68+
69+
No se recomienda esta configuración en un runner compartido ni contra un único host público. El límite `max-rps` suele ser más importante que el número de tareas.
70+
71+
## Reanudación
72+
73+
```bash
74+
samcloud-m3u --start 0 --end 999999 --resume
75+
```
76+
77+
El checkpoint contiene el siguiente SID pendiente. SQLite conserva y actualiza resultados anteriores; la M3U se regenera ordenada y sin duplicados por SID.
78+
79+
## Formato M3U
80+
81+
```m3u
82+
#EXTM3U
83+
#EXTINF:-1 sid="123" group-title="SAM Cloud" content-type="audio/mpeg",Nombre detectado
84+
https://servidor-final.example/live.mp3
85+
```
86+
87+
## GitHub Actions
88+
89+
- `CI`: instala, ejecuta Ruff y las pruebas.
90+
- `Crawl and build M3U`: ejecución manual con rango y límites configurables.
91+
- Programación semanal: usa por defecto `0-9999`, 32 tareas, 16 conexiones por host y 8 peticiones/s.
92+
- Para cambiar el rango programado, crea las variables de repositorio `SAMCLOUD_START` y `SAMCLOUD_END`.
93+
- Los resultados se suben como artifact durante 30 días.
94+
- En una ejecución manual, `publish=true` publica `output/` y el checkpoint en la rama predeterminada.
95+
96+
GitHub advierte que los workflows programados pueden retrasarse en periodos de carga y que los runners alojados tienen un máximo de seis horas por job. Por eso conviene usar lotes pequeños y programar fuera del minuto 0.
97+
98+
## Limitaciones
99+
100+
- Algunos servidores antiguos responden con una línea de estado no estándar `ICY 200 OK`; `aiohttp` puede rechazar esos endpoints. Los servidores modernos suelen devolver HTTP normal con cabeceras `icy-*`.
101+
- Cerrar una respuesta de audio tras leer sus cabeceras minimiza ancho de banda, pero sigue contando como una conexión al servicio.
102+
- Una URL que responde correctamente no garantiza que la emisora esté permanentemente activa.
103+
- El proyecto no intenta saltarse autenticación, CAPTCHA, bloqueos, cuotas ni controles de acceso.
104+
105+
## Referencias
106+
107+
- Documentación de clientes y conectores aiohttp: https://docs.aiohttp.org/en/stable/client.html
108+
- Límites de GitHub Actions: https://docs.github.com/actions/reference/limits
109+
- Eventos programados de Actions: https://docs.github.com/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule
110+
- Guías SAM Broadcaster Cloud: https://support.spacial.com/hc/en-us/sections/206508527-User-Guides
111+
112+
## Licencia
113+
114+
MIT. El proyecto no está afiliado a Spacial, Triton Digital ni GitHub.
115+
116+
## Publicar como repositorio nuevo
117+
118+
Con GitHub CLI autenticado:
119+
120+
```bash
121+
./scripts/publish.sh samcloud-m3u-crawler public
122+
```
123+
124+
En PowerShell:
125+
126+
```powershell
127+
.\scripts\publish.ps1 -RepositoryName samcloud-m3u-crawler -Visibility public
128+
```

SECURITY.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Security and responsible use
2+
3+
- No uses este proyecto para eludir autenticación o controles de acceso.
4+
- No ejecutes enumeraciones amplias sin autorización del operador del servicio.
5+
- Mantén límites bajos de `per-host` y `max-rps`.
6+
- Detén la ejecución ante HTTP 429 persistentes o cualquier petición del proveedor.
7+
- No publiques datos privados, tokens, cookies ni URLs firmadas.

data/.gitkeep

Whitespace-only changes.

output/.gitkeep

Whitespace-only changes.

pyproject.toml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[build-system]
2+
requires = ["setuptools>=75", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "samcloud-m3u-crawler"
7+
version = "0.1.0"
8+
description = "Crawler responsable de enlaces de escucha SAM Cloud y generador M3U"
9+
readme = "README.md"
10+
requires-python = ">=3.11"
11+
license = { text = "MIT" }
12+
authors = [{ name = "Joaquín Rufo Gutiérrez" }]
13+
dependencies = [
14+
"aiohttp>=3.12,<4",
15+
]
16+
17+
[project.optional-dependencies]
18+
dev = [
19+
"pytest>=8,<10",
20+
"pytest-asyncio>=0.24,<2",
21+
"ruff>=0.9,<1",
22+
]
23+
24+
[project.scripts]
25+
samcloud-m3u = "samcloud_m3u_crawler.cli:main"
26+
27+
[tool.setuptools.packages.find]
28+
where = ["src"]
29+
30+
[tool.pytest.ini_options]
31+
asyncio_mode = "auto"
32+
testpaths = ["tests"]
33+
34+
[tool.ruff]
35+
line-length = 100
36+
target-version = "py311"
37+
38+
[tool.ruff.lint]
39+
select = ["E", "F", "I", "UP", "B", "SIM"]

0 commit comments

Comments
 (0)