Update app.py
Browse files
app.py
CHANGED
|
@@ -8,6 +8,7 @@ from collections import Counter
|
|
| 8 |
|
| 9 |
def analyze_and_output(my_id, my_pw, target_id, progress=gr.Progress()):
|
| 10 |
try:
|
|
|
|
| 11 |
client = Client()
|
| 12 |
my_id = my_id.replace('@', '').strip()
|
| 13 |
target_handle = target_id.replace('@', '').strip()
|
|
@@ -21,35 +22,39 @@ def analyze_and_output(my_id, my_pw, target_id, progress=gr.Progress()):
|
|
| 21 |
repost_users = []
|
| 22 |
like_users = []
|
| 23 |
|
| 24 |
-
max_limit =
|
| 25 |
|
| 26 |
-
# ---
|
| 27 |
progress(0, desc="フィードを取得中...")
|
| 28 |
cursor = None
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
for feed_view in response.feed:
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
repost_users.append(orig_author)
|
| 41 |
continue
|
| 42 |
|
| 43 |
-
|
| 44 |
-
if post.author.handle == target_handle:
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
continue
|
| 48 |
-
|
| 49 |
rkey = post.uri.split('/')[-1]
|
| 50 |
post_url = f"https://bsky.app/profile/{target_handle}/post/{rkey}"
|
| 51 |
|
| 52 |
-
# 各種カウント(None対策)
|
| 53 |
likes = getattr(post, 'like_count', 0) or 0
|
| 54 |
reposts = getattr(post, 'repost_count', 0) or 0
|
| 55 |
created_at_raw = getattr(post.record, 'created_at', None)
|
|
@@ -64,49 +69,43 @@ def analyze_and_output(my_id, my_pw, target_id, progress=gr.Progress()):
|
|
| 64 |
'score': likes + reposts,
|
| 65 |
'url': post_url
|
| 66 |
})
|
| 67 |
-
|
| 68 |
if text:
|
| 69 |
hashtags.extend(re.findall(r'#(\w+)', text))
|
| 70 |
|
| 71 |
# リプライ相手
|
| 72 |
-
if feed_view
|
| 73 |
parent = feed_view.reply.parent
|
| 74 |
if hasattr(parent, 'author') and parent.author:
|
| 75 |
p_handle = parent.author.handle
|
| 76 |
if p_handle != target_handle:
|
| 77 |
reply_users.append(p_handle)
|
| 78 |
|
| 79 |
-
cursor = response
|
| 80 |
if not cursor or len(posts_data) >= max_limit: break
|
| 81 |
progress(min(len(posts_data)/max_limit * 0.5, 0.5), desc=f"{len(posts_data)}件取得中...")
|
| 82 |
|
| 83 |
-
# ---
|
| 84 |
progress(0.6, desc="いいねを分析中...")
|
| 85 |
-
like_cursor = None
|
| 86 |
try:
|
| 87 |
-
|
| 88 |
-
|
| 89 |
for like_item in likes_resp.feed:
|
| 90 |
if like_item.post and hasattr(like_item.post, 'author') and like_item.post.author:
|
| 91 |
l_handle = like_item.post.author.handle
|
| 92 |
if l_handle != target_handle:
|
| 93 |
like_users.append(l_handle)
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
except Exception:
|
| 97 |
-
pass # いいねが取得できなくても続行
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
|
|
|
| 101 |
|
| 102 |
-
# --- 3. 解析とHTML作成 ---
|
| 103 |
df = pd.DataFrame(posts_data)
|
| 104 |
df = df.sort_values('created_at', ascending=True)
|
| 105 |
-
first_post = df.iloc[0]
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
days_active = max(delta.days, 1)
|
| 110 |
|
| 111 |
stats_html = f"""
|
| 112 |
<div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px;'>
|
|
@@ -141,29 +140,42 @@ def analyze_and_output(my_id, my_pw, target_id, progress=gr.Progress()):
|
|
| 141 |
</div>
|
| 142 |
"""
|
| 143 |
|
| 144 |
-
# ベストポスト Top 3
|
| 145 |
top_3 = df.sort_values('score', ascending=False).head(3)
|
| 146 |
-
posts_html = f""
|
| 147 |
-
|
| 148 |
-
<small style='color: #ffa000; font-weight: bold;'>🌱 取得範囲内の初投稿 ({first_post['created_at'].strftime('%Y/%m/%d')})</small>
|
| 149 |
-
<p style='font-size: 0.9em; margin: 5px 0;'>{first_post['text']}</p>
|
| 150 |
-
<a href='{first_post['url']}' target='_blank' style='font-size: 0.8em; color: #ff8f00;'>投稿をみる</a>
|
| 151 |
-
</div>
|
| 152 |
-
<h4 style='color: #d81b60; margin-bottom: 10px;'>🏆 ベストポスト Top 3</h4>
|
| 153 |
-
"""
|
| 154 |
for i, (_, row) in enumerate(top_3.iterrows(), 1):
|
| 155 |
posts_html += f"""
|
| 156 |
-
<div style='border: 1px solid #
|
| 157 |
-
<
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
</div>
|
| 161 |
-
<p style='font-size: 0.9em; margin: 8px 0;'>{row['text'][:100]}...</p>
|
| 162 |
-
<a href='{row['url']}' target='_blank' style='color: #d81b60; font-size: 0.85em;'>投稿をみる</a>
|
| 163 |
</div>
|
| 164 |
"""
|
| 165 |
|
| 166 |
-
return stats_html, rank_html, posts_html, "
|
| 167 |
|
| 168 |
except Exception as e:
|
| 169 |
-
return f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
def analyze_and_output(my_id, my_pw, target_id, progress=gr.Progress()):
|
| 10 |
try:
|
| 11 |
+
# --- 1. ログイン ---
|
| 12 |
client = Client()
|
| 13 |
my_id = my_id.replace('@', '').strip()
|
| 14 |
target_handle = target_id.replace('@', '').strip()
|
|
|
|
| 22 |
repost_users = []
|
| 23 |
like_users = []
|
| 24 |
|
| 25 |
+
max_limit = 500
|
| 26 |
|
| 27 |
+
# --- 2. 投稿フィードの分析 ---
|
| 28 |
progress(0, desc="フィードを取得中...")
|
| 29 |
cursor = None
|
| 30 |
+
|
| 31 |
+
for _ in range(10):
|
| 32 |
+
try:
|
| 33 |
+
response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
|
| 34 |
+
except Exception:
|
| 35 |
+
break
|
| 36 |
+
|
| 37 |
+
if not response or not hasattr(response, 'feed') or not response.feed:
|
| 38 |
+
break
|
| 39 |
+
|
| 40 |
for feed_view in response.feed:
|
| 41 |
+
if not getattr(feed_view, 'post', None): continue
|
| 42 |
+
post = feed_view.post
|
| 43 |
+
|
| 44 |
+
# リポスト分析
|
| 45 |
+
if feed_view.reason:
|
| 46 |
+
if hasattr(post, 'author') and post.author:
|
| 47 |
+
if post.author.handle != target_handle:
|
| 48 |
+
repost_users.append(post.author.handle)
|
|
|
|
| 49 |
continue
|
| 50 |
|
| 51 |
+
# 本人投稿分析
|
| 52 |
+
if hasattr(post, 'author') and post.author and post.author.handle == target_handle:
|
| 53 |
+
if not hasattr(post, 'record'): continue
|
| 54 |
+
|
|
|
|
|
|
|
| 55 |
rkey = post.uri.split('/')[-1]
|
| 56 |
post_url = f"https://bsky.app/profile/{target_handle}/post/{rkey}"
|
| 57 |
|
|
|
|
| 58 |
likes = getattr(post, 'like_count', 0) or 0
|
| 59 |
reposts = getattr(post, 'repost_count', 0) or 0
|
| 60 |
created_at_raw = getattr(post.record, 'created_at', None)
|
|
|
|
| 69 |
'score': likes + reposts,
|
| 70 |
'url': post_url
|
| 71 |
})
|
|
|
|
| 72 |
if text:
|
| 73 |
hashtags.extend(re.findall(r'#(\w+)', text))
|
| 74 |
|
| 75 |
# リプライ相手
|
| 76 |
+
if getattr(feed_view, 'reply', None) and feed_view.reply.parent:
|
| 77 |
parent = feed_view.reply.parent
|
| 78 |
if hasattr(parent, 'author') and parent.author:
|
| 79 |
p_handle = parent.author.handle
|
| 80 |
if p_handle != target_handle:
|
| 81 |
reply_users.append(p_handle)
|
| 82 |
|
| 83 |
+
cursor = getattr(response, 'cursor', None)
|
| 84 |
if not cursor or len(posts_data) >= max_limit: break
|
| 85 |
progress(min(len(posts_data)/max_limit * 0.5, 0.5), desc=f"{len(posts_data)}件取得中...")
|
| 86 |
|
| 87 |
+
# --- 3. いいね一覧の分析 ---
|
| 88 |
progress(0.6, desc="いいねを分析中...")
|
|
|
|
| 89 |
try:
|
| 90 |
+
likes_resp = client.get_actor_likes(actor=profile.did, limit=50)
|
| 91 |
+
if likes_resp and hasattr(likes_resp, 'feed'):
|
| 92 |
for like_item in likes_resp.feed:
|
| 93 |
if like_item.post and hasattr(like_item.post, 'author') and like_item.post.author:
|
| 94 |
l_handle = like_item.post.author.handle
|
| 95 |
if l_handle != target_handle:
|
| 96 |
like_users.append(l_handle)
|
| 97 |
+
except:
|
| 98 |
+
pass
|
|
|
|
|
|
|
| 99 |
|
| 100 |
+
# --- 4. 解析とHTML作成 ---
|
| 101 |
+
if not posts_data:
|
| 102 |
+
return "有効なデータが見つかりませんでした。", "", "", "完了(データ不足)"
|
| 103 |
|
|
|
|
| 104 |
df = pd.DataFrame(posts_data)
|
| 105 |
df = df.sort_values('created_at', ascending=True)
|
|
|
|
| 106 |
|
| 107 |
+
first_post_time = df.iloc[0]['created_at'].replace(tzinfo=None)
|
| 108 |
+
days_active = max((datetime.now().replace(tzinfo=None) - first_post_time).days, 1)
|
|
|
|
| 109 |
|
| 110 |
stats_html = f"""
|
| 111 |
<div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px;'>
|
|
|
|
| 140 |
</div>
|
| 141 |
"""
|
| 142 |
|
|
|
|
| 143 |
top_3 = df.sort_values('score', ascending=False).head(3)
|
| 144 |
+
posts_html = f"<h4>🌱 初投稿 ({df.iloc[0]['created_at'].strftime('%Y/%m/%d')})</h4><p style='font-size: 0.9em; background:#f9f9f9; padding:10px; border-radius:5px;'>{df.iloc[0]['text']}</p><h4>🏆 ベストポスト Top 3</h4>"
|
| 145 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
for i, (_, row) in enumerate(top_3.iterrows(), 1):
|
| 147 |
posts_html += f"""
|
| 148 |
+
<div style='border: 1px solid #eee; padding: 10px; margin-bottom: 8px; border-radius: 8px; background: white;'>
|
| 149 |
+
<b>第{i}位</b> (❤️{row['likes']} 🔄{row['reposts']})<br>
|
| 150 |
+
<p style='margin:5px 0;'>{row['text'][:80]}...</p>
|
| 151 |
+
<a href='{row['url']}' target='_blank' style='color:#d81b60; font-size:0.8em;'>表示</a>
|
|
|
|
|
|
|
|
|
|
| 152 |
</div>
|
| 153 |
"""
|
| 154 |
|
| 155 |
+
return stats_html, rank_html, posts_html, "解析が完了しました!"
|
| 156 |
|
| 157 |
except Exception as e:
|
| 158 |
+
return f"エラーが発生しました: {str(e)}", "", "", "失敗"
|
| 159 |
+
|
| 160 |
+
# --- UI ---
|
| 161 |
+
with gr.Blocks(title="Bluesky Analysis") as demo:
|
| 162 |
+
gr.Markdown("# 🦋 Bluesky 自己分析ダッシュボード")
|
| 163 |
+
with gr.Row():
|
| 164 |
+
with gr.Column(scale=1):
|
| 165 |
+
in_id = gr.Textbox(label="自分のハンドル", placeholder="example.bsky.social")
|
| 166 |
+
in_pw = gr.Textbox(label="アプリパスワード", type="password")
|
| 167 |
+
in_target = gr.Textbox(label="解析したい相手のID")
|
| 168 |
+
btn = gr.Button("分析開始", variant="primary")
|
| 169 |
+
status = gr.Textbox(label="ステータス", interactive=False)
|
| 170 |
+
with gr.Column(scale=2):
|
| 171 |
+
out_stats = gr.HTML()
|
| 172 |
+
out_rank = gr.HTML()
|
| 173 |
+
out_posts = gr.HTML()
|
| 174 |
+
|
| 175 |
+
btn.click(
|
| 176 |
+
fn=analyze_and_output,
|
| 177 |
+
inputs=[in_id, in_pw, in_target],
|
| 178 |
+
outputs=[out_stats, out_rank, out_posts, status]
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
demo.launch()
|