mirror of
https://github.com/NohamR/AFP-RSS.git
synced 2026-07-11 18:59:56 +00:00
Add docker-compose and lint main.py
This commit is contained in:
18
README.md
18
README.md
@@ -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
7
docker-compose.yml
Normal 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
140
main.py
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user