Add time-based media group merging for RSS and HTML generation
This commit is contained in:
+17
-3
@@ -520,7 +520,13 @@ async def get_media(channel: str, post_id: int, file_unique_id: str, digest: str
|
|||||||
|
|
||||||
@app.get("/rss/{channel}", response_class=Response)
|
@app.get("/rss/{channel}", response_class=Response)
|
||||||
@app.get("/rss/{channel}/{token}", response_class=Response)
|
@app.get("/rss/{channel}/{token}", response_class=Response)
|
||||||
async def get_rss_feed(channel: str, token: str | None = None, limit: int = 50, output_type: str = 'rss', exclude_flags: str | None = None):
|
async def get_rss_feed(channel: str,
|
||||||
|
token: str | None = None,
|
||||||
|
limit: int = 50,
|
||||||
|
output_type: str = 'rss',
|
||||||
|
exclude_flags: str | None = None,
|
||||||
|
merge_seconds: int = 5
|
||||||
|
):
|
||||||
if Config["token"]:
|
if Config["token"]:
|
||||||
if token != Config["token"]:
|
if token != Config["token"]:
|
||||||
logger.error(f"Invalid token for RSS feed: {token}, expected: {Config['token']}")
|
logger.error(f"Invalid token for RSS feed: {token}, expected: {Config['token']}")
|
||||||
@@ -530,10 +536,18 @@ async def get_rss_feed(channel: str, token: str | None = None, limit: int = 50,
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if output_type == 'rss':
|
if output_type == 'rss':
|
||||||
rss_content = await generate_channel_rss(channel, client=client.client, limit=limit, exclude_flags=exclude_flags)
|
rss_content = await generate_channel_rss(channel,
|
||||||
|
client=client.client,
|
||||||
|
limit=limit,
|
||||||
|
exclude_flags=exclude_flags,
|
||||||
|
merge_seconds=merge_seconds)
|
||||||
return Response(content=rss_content, media_type="application/xml")
|
return Response(content=rss_content, media_type="application/xml")
|
||||||
elif output_type == 'html':
|
elif output_type == 'html':
|
||||||
rss_content = await generate_channel_html(channel, client=client.client, limit=limit, exclude_flags=exclude_flags)
|
rss_content = await generate_channel_html(channel,
|
||||||
|
client=client.client,
|
||||||
|
limit=limit,
|
||||||
|
exclude_flags=exclude_flags,
|
||||||
|
merge_seconds=merge_seconds)
|
||||||
return Response(content=rss_content, media_type="text/html")
|
return Response(content=rss_content, media_type="text/html")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
error_message = f"Invalid parameters for RSS feed generation: {str(e)}"
|
error_message = f"Invalid parameters for RSS feed generation: {str(e)}"
|
||||||
|
|||||||
@@ -11,5 +11,6 @@ def get_settings():
|
|||||||
"pyrogram_bridge_url": os.getenv("PYROGRAM_BRIDGE_URL", ""),
|
"pyrogram_bridge_url": os.getenv("PYROGRAM_BRIDGE_URL", ""),
|
||||||
"log_level": os.getenv("LOG_LEVEL", "INFO"),
|
"log_level": os.getenv("LOG_LEVEL", "INFO"),
|
||||||
"debug": os.getenv("DEBUG", "False") == "True",
|
"debug": os.getenv("DEBUG", "False") == "True",
|
||||||
"token": os.getenv("TOKEN", "")
|
"token": os.getenv("TOKEN", ""),
|
||||||
|
"time_based_merge": os.getenv("TIME_BASED_MERGE", "False") == "True",
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-12
@@ -404,9 +404,9 @@ class PostParser:
|
|||||||
|
|
||||||
def _generate_html_footer(self, message: Message) -> str:
|
def _generate_html_footer(self, message: Message) -> str:
|
||||||
content_footer = []
|
content_footer = []
|
||||||
if reactions_views_html := self._reactions_views_links(message): # Add reactions, views and links
|
if reactions_views_html := self._reactions_views_links(message): # Add reactions, views, date and links
|
||||||
content_footer.append(reactions_views_html)
|
content_footer.append(reactions_views_html)
|
||||||
if flags_html := self._format_flags(message): # Add flags
|
if flags_html := self._format_flags(message): # Add flags
|
||||||
content_footer.append(flags_html)
|
content_footer.append(flags_html)
|
||||||
html_footer = '\n'.join(content_footer)
|
html_footer = '\n'.join(content_footer)
|
||||||
html_footer = self._sanitize_html(html_footer)
|
html_footer = self._sanitize_html(html_footer)
|
||||||
@@ -475,14 +475,19 @@ class PostParser:
|
|||||||
if views := getattr(message, "views", None):
|
if views := getattr(message, "views", None):
|
||||||
views_html = f'<span class="views">{views} views</span>'
|
views_html = f'<span class="views">{views} views</span>'
|
||||||
|
|
||||||
# If both reactions and views exist, insert a line break between them.
|
# Combine reactions and views on the same line
|
||||||
if reactions_html and views_html:
|
if reactions_html or views_html:
|
||||||
parts.append(reactions_html + "<br>" + views_html)
|
parts.append(f'{reactions_html} {views_html}')
|
||||||
elif reactions_html:
|
|
||||||
parts.append(reactions_html)
|
|
||||||
elif views_html:
|
|
||||||
parts.append(views_html)
|
|
||||||
|
|
||||||
|
# Add date and links on the same line
|
||||||
|
second_line_parts = []
|
||||||
|
|
||||||
|
# Format date as DD/MM/YY, HH:MM:SS
|
||||||
|
if message.date:
|
||||||
|
formatted_date = message.date.strftime("%d/%m/%y, %H:%M:%S")
|
||||||
|
second_line_parts.append(formatted_date)
|
||||||
|
|
||||||
|
# Add links
|
||||||
channel_identifier = self.get_channel_username(message)
|
channel_identifier = self.get_channel_username(message)
|
||||||
if channel_identifier:
|
if channel_identifier:
|
||||||
links = []
|
links = []
|
||||||
@@ -490,12 +495,15 @@ class PostParser:
|
|||||||
channel_id = channel_identifier[4:] # Remove '-100' prefix for web links
|
channel_id = channel_identifier[4:] # Remove '-100' prefix for web links
|
||||||
links.append(f'<a href="tg://resolve?domain=c/{channel_id}&post={message.id}">Open in Telegram</a>')
|
links.append(f'<a href="tg://resolve?domain=c/{channel_id}&post={message.id}">Open in Telegram</a>')
|
||||||
links.append(f'<a href="https://t.me/c/{channel_id}/{message.id}">Open in Web</a>')
|
links.append(f'<a href="https://t.me/c/{channel_id}/{message.id}">Open in Web</a>')
|
||||||
else: # For channels with username
|
else: # For channels with username
|
||||||
links.append(f'<a href="tg://resolve?domain={channel_identifier}&post={message.id}">Open in Telegram</a>')
|
links.append(f'<a href="tg://resolve?domain={channel_identifier}&post={message.id}">Open in Telegram</a>')
|
||||||
links.append(f'<a href="https://t.me/{channel_identifier}/{message.id}">Open in Web</a>')
|
links.append(f'<a href="https://t.me/{channel_identifier}/{message.id}">Open in Web</a>')
|
||||||
parts.append(' '.join(links))
|
second_line_parts.extend(links)
|
||||||
|
|
||||||
html = ' '.join(parts) if parts else None
|
if second_line_parts:
|
||||||
|
parts.append(' '.join(second_line_parts))
|
||||||
|
|
||||||
|
html = '<br>'.join(parts) if parts else None
|
||||||
return f"<br>{html}" if html else None
|
return f"<br>{html}" if html else None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+75
-9
@@ -16,6 +16,49 @@ if not logger.handlers:
|
|||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_time_based_media_groups(messages, merge_seconds: int = 5):
|
||||||
|
messages_sorted = sorted(messages, key=lambda x: x.date)
|
||||||
|
cluster = []
|
||||||
|
last_msg_date = None
|
||||||
|
current_media_group_id = None
|
||||||
|
|
||||||
|
for msg in messages_sorted:
|
||||||
|
|
||||||
|
if not cluster:
|
||||||
|
cluster.append(msg)
|
||||||
|
last_msg_date = msg.date
|
||||||
|
current_media_group_id = getattr(msg, "media_group_id", None)
|
||||||
|
continue
|
||||||
|
|
||||||
|
time_diff = (msg.date - last_msg_date).total_seconds()
|
||||||
|
|
||||||
|
msg_media_group_id = getattr(msg, "media_group_id", None)
|
||||||
|
|
||||||
|
if time_diff <= merge_seconds:
|
||||||
|
if current_media_group_id:
|
||||||
|
msg.media_group_id = current_media_group_id
|
||||||
|
elif msg_media_group_id:
|
||||||
|
current_media_group_id = msg_media_group_id
|
||||||
|
for m in cluster:
|
||||||
|
m.media_group_id = current_media_group_id
|
||||||
|
cluster.append(msg)
|
||||||
|
last_msg_date = msg.date
|
||||||
|
else:
|
||||||
|
if len(cluster) >= 2 and not current_media_group_id:
|
||||||
|
new_group_id = f"time_{min(m.date for m in cluster)}"
|
||||||
|
for m in cluster:
|
||||||
|
m.media_group_id = new_group_id
|
||||||
|
cluster = [msg]
|
||||||
|
last_msg_date = msg.date
|
||||||
|
current_media_group_id = msg_media_group_id
|
||||||
|
|
||||||
|
if len(cluster) >= 2 and not current_media_group_id:
|
||||||
|
new_group_id = f"time_{min(m.date for m in cluster)}"
|
||||||
|
for m in cluster:
|
||||||
|
m.media_group_id = new_group_id
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
async def create_messages_groups(messages):
|
async def create_messages_groups(messages):
|
||||||
"""
|
"""
|
||||||
Process messages into formatted posts, handling media groups
|
Process messages into formatted posts, handling media groups
|
||||||
@@ -96,22 +139,29 @@ async def render_messages_groups(messages_groups, post_parser, exclude_flags: st
|
|||||||
'author': message_data['author'],
|
'author': message_data['author'],
|
||||||
'flags': message_data['flags']
|
'flags': message_data['flags']
|
||||||
})
|
})
|
||||||
else: # Multiple messages in group - need to merge
|
else: # Multiple messages in group - merge media, text and html body
|
||||||
processed_messages = [post_parser.process_message(msg) for msg in group]
|
processed_messages = [post_parser.process_message(msg) for msg in group]
|
||||||
main_message = next( # Find main message (one with text)
|
main_message = next( # Determine main message for header/footer/title
|
||||||
(msg for msg in processed_messages if msg['text']),
|
(msg for msg in processed_messages if msg['text']),
|
||||||
processed_messages[0] # fallback to first if none has text
|
processed_messages[0] # fallback if no message contains text
|
||||||
)
|
)
|
||||||
|
|
||||||
# Collect all media sections
|
# Collect all media sections from all messages in the group
|
||||||
all_media = [msg['html']['media'] for msg in processed_messages if msg['html']['media']]
|
all_media = [msg['html']['media'] for msg in processed_messages if msg['html']['media']]
|
||||||
combined_media = '\n'.join(all_media)
|
combined_media = '\n'.join(all_media)
|
||||||
|
|
||||||
# Combine parts
|
# Merge text fields from all messages
|
||||||
|
all_texts = [msg['text'] for msg in processed_messages if msg['text']]
|
||||||
|
combined_text = '\n'.join(all_texts)
|
||||||
|
|
||||||
|
# Merge html body sections from all messages
|
||||||
|
all_html_bodies = [msg['html']['body'] for msg in processed_messages if msg['html']['body']]
|
||||||
|
combined_html_body = '\n'.join(all_html_bodies)
|
||||||
|
|
||||||
html_parts = [
|
html_parts = [
|
||||||
f'<div class="message-header">{main_message["html"]["header"]}</div>',
|
f'<div class="message-header">{main_message["html"]["header"]}</div>',
|
||||||
f'<div class="message-media">{combined_media}</div>',
|
f'<div class="message-media">{combined_media}</div>',
|
||||||
f'<div class="message-text">{main_message["html"]["body"]}</div>',
|
f'<div class="message-text">{combined_html_body}</div>',
|
||||||
f'<div class="message-footer">{main_message["html"]["footer"]}</div>'
|
f'<div class="message-footer">{main_message["html"]["footer"]}</div>'
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -120,7 +170,7 @@ async def render_messages_groups(messages_groups, post_parser, exclude_flags: st
|
|||||||
'date': main_message['date'],
|
'date': main_message['date'],
|
||||||
'message_id': main_message['message_id'],
|
'message_id': main_message['message_id'],
|
||||||
'title': main_message['html']['title'],
|
'title': main_message['html']['title'],
|
||||||
'text': main_message['text'],
|
'text': combined_text,
|
||||||
'author': main_message['author'],
|
'author': main_message['author'],
|
||||||
'flags': main_message['flags']
|
'flags': main_message['flags']
|
||||||
})
|
})
|
||||||
@@ -147,7 +197,13 @@ async def render_messages_groups(messages_groups, post_parser, exclude_flags: st
|
|||||||
rendered_posts.sort(key=lambda x: x['date'], reverse=True)
|
rendered_posts.sort(key=lambda x: x['date'], reverse=True)
|
||||||
return rendered_posts
|
return rendered_posts
|
||||||
|
|
||||||
async def generate_channel_rss(channel: str, post_parser: Optional[PostParser] = None, client = None, limit: int = 20, exclude_flags: str | None = None ) -> str:
|
async def generate_channel_rss(channel: str,
|
||||||
|
post_parser: Optional[PostParser] = None,
|
||||||
|
client = None,
|
||||||
|
limit: int = 20,
|
||||||
|
exclude_flags: str | None = None,
|
||||||
|
merge_seconds: int = 5
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generate RSS feed for channel using actual messages
|
Generate RSS feed for channel using actual messages
|
||||||
Args:
|
Args:
|
||||||
@@ -199,6 +255,8 @@ async def generate_channel_rss(channel: str, post_parser: Optional[PostParser] =
|
|||||||
messages.append(message)
|
messages.append(message)
|
||||||
|
|
||||||
# Process messages into groups and render them
|
# Process messages into groups and render them
|
||||||
|
if Config['time_based_merge']:
|
||||||
|
messages = await create_time_based_media_groups(messages, merge_seconds)
|
||||||
message_groups = await create_messages_groups(messages)
|
message_groups = await create_messages_groups(messages)
|
||||||
message_groups = await trim_messages_groups(message_groups, limit)
|
message_groups = await trim_messages_groups(message_groups, limit)
|
||||||
final_posts = await render_messages_groups(message_groups, post_parser, exclude_flags)
|
final_posts = await render_messages_groups(message_groups, post_parser, exclude_flags)
|
||||||
@@ -231,7 +289,13 @@ async def generate_channel_rss(channel: str, post_parser: Optional[PostParser] =
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def generate_channel_html(channel: str, post_parser: Optional[PostParser] = None, client = None, limit: int = 20, exclude_flags: str | None = None) -> str:
|
async def generate_channel_html(channel: str,
|
||||||
|
post_parser: Optional[PostParser] = None,
|
||||||
|
client = None,
|
||||||
|
limit: int = 20,
|
||||||
|
exclude_flags: str | None = None,
|
||||||
|
merge_seconds: int = 5
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generate HTML feed for channel using actual messages
|
Generate HTML feed for channel using actual messages
|
||||||
Args:
|
Args:
|
||||||
@@ -272,6 +336,8 @@ async def generate_channel_html(channel: str, post_parser: Optional[PostParser]
|
|||||||
messages.append(message)
|
messages.append(message)
|
||||||
|
|
||||||
# Process messages into groups and render them
|
# Process messages into groups and render them
|
||||||
|
if Config['time_based_merge']:
|
||||||
|
messages = await create_time_based_media_groups(messages, merge_seconds)
|
||||||
message_groups = await create_messages_groups(messages)
|
message_groups = await create_messages_groups(messages)
|
||||||
message_groups = await trim_messages_groups(message_groups, limit)
|
message_groups = await trim_messages_groups(message_groups, limit)
|
||||||
final_posts = await render_messages_groups(message_groups, post_parser, exclude_flags)
|
final_posts = await render_messages_groups(message_groups, post_parser, exclude_flags)
|
||||||
|
|||||||
Reference in New Issue
Block a user