Add docker-compose and lint main.py

This commit is contained in:
√(noham)²
2026-05-25 15:10:25 +02:00
parent 0c64b0200a
commit 502d770749
3 changed files with 109 additions and 56 deletions

View File

@@ -20,7 +20,23 @@ uv run python main.py
## Running with Docker ## Running with Docker
You can use the pre-built image directly from Docker Hub:
```bash
docker run -d -p 8080:8080 --name afp-rss nohamr/afp-rss:latest
```
### Using Docker Compose
A `docker-compose.yml` file is provided for convenience:
```bash
docker compose up -d
```
### Building the Image Yourself
```bash ```bash
docker build -t afp-rss . docker build -t afp-rss .
docker run -d -p 8080:8080 --name afp-rss afp-rss docker run -d -p 8080:8080 --name afp-rss afp-rss
``` ```

7
docker-compose.yml Normal file
View File

@@ -0,0 +1,7 @@
services:
afp-rss:
image: nohamr/afp-rss:latest
container_name: afp-rss
ports:
- "8080:8080"
restart: unless-stopped

140
main.py
View File

@@ -10,75 +10,99 @@ from bs4 import BeautifulSoup
news_list = [] news_list = []
LAST_UPDATE = "" LAST_UPDATE = ""
def fetch_news(): def fetch_news():
global news_list, LAST_UPDATE global news_list, LAST_UPDATE
session = requests.Session() session = requests.Session()
headers = { headers = {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
'accept-language': 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7', "accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
'upgrade-insecure-requests': '1', "upgrade-insecure-requests": "1",
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36', "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",
} }
try: try:
response = session.get('https://www.mediapart.fr/journal/fil-dactualites', headers=headers) response = session.get(
"https://www.mediapart.fr/journal/fil-dactualites", headers=headers
)
response.raise_for_status() response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser') soup = BeautifulSoup(response.content, "html.parser")
articles = soup.find_all('div', class_='teaser') articles = soup.find_all("div", class_="teaser")
new_news_list = [] new_news_list = []
for article in articles: for article in articles:
title_tag = article.find('h2', class_='teaser__title') title_tag = article.find("h2", class_="teaser__title")
titre = title_tag.get_text(separator=" ", strip=True) if title_tag else "Non trouvé" titre = (
title_tag.get_text(separator=" ", strip=True)
if title_tag
else "Non trouvé"
)
body_tag = article.find('div', class_='teaser__body') body_tag = article.find("div", class_="teaser__body")
resume = body_tag.get_text(separator=" ", strip=True) if body_tag else "Non trouvé" resume = (
body_tag.get_text(separator=" ", strip=True)
if body_tag
else "Non trouvé"
)
date_tag = article.find("time", class_="teaser__timestamp")
date_iso = (
date_tag.get("datetime", datetime.now(timezone.utc).isoformat())
if date_tag
else datetime.now(timezone.utc).isoformat()
)
link_tag = article.find("a")
link = (
f"https://www.mediapart.fr{link_tag['href']}"
if link_tag and "href" in link_tag.attrs
else "https://www.mediapart.fr"
)
date_tag = article.find('time', class_='teaser__timestamp')
date_iso = date_tag.get('datetime', datetime.now(timezone.utc).isoformat()) if date_tag else datetime.now(timezone.utc).isoformat()
link_tag = article.find('a')
link = f"https://www.mediapart.fr{link_tag['href']}" if link_tag and 'href' in link_tag.attrs else "https://www.mediapart.fr"
# Use link as id # Use link as id
new_news_list.append({ new_news_list.append(
'title': titre, {
'content': resume, "title": titre,
'date_modified': date_iso, "content": resume,
'url': link, "date_modified": date_iso,
'id': link "url": link,
}) "id": link,
}
)
news_list = new_news_list news_list = new_news_list
LAST_UPDATE = datetime.now(timezone.utc).isoformat() LAST_UPDATE = datetime.now(timezone.utc).isoformat()
print(f"[{LAST_UPDATE}] News fetched successfully ({len(news_list)} items).") print(f"[{LAST_UPDATE}] News fetched successfully ({len(news_list)} items).")
except Exception as e: except Exception as e:
print(f"[{datetime.now(timezone.utc).isoformat()}] Error fetching news: {e}") print(f"[{datetime.now(timezone.utc).isoformat()}] Error fetching news: {e}")
def update_loop(): def update_loop():
while True: while True:
fetch_news() fetch_news()
time.sleep(3600) # Update periodically (every hour) time.sleep(3600) # Update periodically (every hour)
def build_atom(): def build_atom():
fg = FeedGenerator() fg = FeedGenerator()
fg.id("https://www.mediapart.fr/journal/fil-dactualites") fg.id("https://www.mediapart.fr/journal/fil-dactualites")
fg.title("Mediapart Fil d'actualités") fg.title("Mediapart Fil d'actualités")
fg.author({'name': 'Mediapart'}) fg.author({"name": "Mediapart"})
fg.link(href="https://www.mediapart.fr", rel="alternate") fg.link(href="https://www.mediapart.fr", rel="alternate")
fg.updated(LAST_UPDATE) fg.updated(LAST_UPDATE)
for item in news_list: for item in news_list:
fe = fg.add_entry() fe = fg.add_entry()
fe.id(item['id']) fe.id(item["id"])
fe.title(item['title']) fe.title(item["title"])
fe.link(href=item['url'], rel="alternate") fe.link(href=item["url"], rel="alternate")
fe.published(item['date_modified']) fe.published(item["date_modified"])
fe.updated(item['date_modified']) fe.updated(item["date_modified"])
fe.content(item['content'], type="html") fe.content(item["content"], type="html")
return fg.atom_str(pretty=True) return fg.atom_str(pretty=True)
def build_json(): def build_json():
data = { data = {
"version": "https://jsonfeed.org/version/1", "version": "https://jsonfeed.org/version/1",
@@ -90,12 +114,13 @@ def build_json():
"title": item["title"], "title": item["title"],
"date_modified": item["date_modified"], "date_modified": item["date_modified"],
"url": item["url"], "url": item["url"],
"content_text": item["content"] "content_text": item["content"],
} }
for item in news_list for item in news_list
] ],
} }
return json.dumps(data, indent=4).encode('utf-8') return json.dumps(data, indent=4).encode("utf-8")
def build_txt(): def build_txt():
# Emulate PHP print_r output for text like in the example # Emulate PHP print_r output for text like in the example
@@ -106,7 +131,7 @@ def build_txt():
lines.append(" [uri] => https://www.mediapart.fr") lines.append(" [uri] => https://www.mediapart.fr")
lines.append(" [items] => Array") lines.append(" [items] => Array")
lines.append(" (") lines.append(" (")
for i, item in enumerate(news_list): for i, item in enumerate(news_list):
lines.append(f" [{i}] => Array") lines.append(f" [{i}] => Array")
lines.append(" (") lines.append(" (")
@@ -116,10 +141,11 @@ def build_txt():
lines.append(f" [content] => {item['content']}") lines.append(f" [content] => {item['content']}")
lines.append(" )") lines.append(" )")
lines.append("") lines.append("")
lines.append(" )") lines.append(" )")
lines.append(")") lines.append(")")
return "\n".join(lines).encode('utf-8') return "\n".join(lines).encode("utf-8")
class RSSRequestHandler(BaseHTTPRequestHandler): class RSSRequestHandler(BaseHTTPRequestHandler):
# Hide log requests to not spam console # Hide log requests to not spam console
@@ -127,24 +153,24 @@ class RSSRequestHandler(BaseHTTPRequestHandler):
pass pass
def do_GET(self): def do_GET(self):
if self.path == '/rss.atom': if self.path == "/rss.atom":
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'application/atom+xml; charset=utf-8') self.send_header("Content-Type", "application/atom+xml; charset=utf-8")
self.end_headers() self.end_headers()
self.wfile.write(build_atom()) self.wfile.write(build_atom())
elif self.path == '/rss.json': elif self.path == "/rss.json":
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'application/json; charset=utf-8') self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers() self.end_headers()
self.wfile.write(build_json()) self.wfile.write(build_json())
elif self.path == '/rss.txt': elif self.path == "/rss.txt":
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'text/plain; charset=utf-8') self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers() self.end_headers()
self.wfile.write(build_txt()) self.wfile.write(build_txt())
else: else:
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8') self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers() self.end_headers()
html = """ html = """
<html><body> <html><body>
@@ -156,24 +182,28 @@ class RSSRequestHandler(BaseHTTPRequestHandler):
</ul> </ul>
</body></html> </body></html>
""" """
self.wfile.write(html.encode('utf-8')) self.wfile.write(html.encode("utf-8"))
def main(): def main():
print("Starting initial fetch...") print("Starting initial fetch...")
fetch_news() fetch_news()
thread = threading.Thread(target=update_loop, daemon=True) thread = threading.Thread(target=update_loop, daemon=True)
thread.start() thread.start()
server_address = ('', 8080) server_address = ("", 8080)
httpd = HTTPServer(server_address, RSSRequestHandler) httpd = HTTPServer(server_address, RSSRequestHandler)
print("Serving feeds at port 8080.") print("Serving feeds at port 8080.")
print("Paths available: http://localhost:8080/rss.atom, http://localhost:8080/rss.json, http://localhost:8080/rss.txt") print(
"Paths available: http://localhost:8080/rss.atom, http://localhost:8080/rss.json, http://localhost:8080/rss.txt"
)
try: try:
httpd.serve_forever() httpd.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nShutting down server.") print("\nShutting down server.")
httpd.server_close() httpd.server_close()
if __name__ == '__main__':
main() if __name__ == "__main__":
main()