Kanhshsh commited on
Commit
a0c5538
Β·
verified Β·
1 Parent(s): e432d84

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +653 -0
app.py ADDED
@@ -0,0 +1,653 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ import uuid
4
+ import time
5
+ import json
6
+ import base64
7
+ import asyncio
8
+ import logging
9
+ import atexit
10
+ import numpy as np
11
+ import requests
12
+ from flask import Flask, render_template, request, redirect
13
+ from threading import Thread
14
+ from pymongo import MongoClient
15
+ from google.oauth2.credentials import Credentials
16
+ from google.auth.transport.requests import Request
17
+ from googleapiclient.discovery import build
18
+ from googleapiclient.http import MediaFileUpload
19
+ from moviepy.editor import VideoFileClip, ColorClip, CompositeVideoClip, ImageClip
20
+ from moviepy.video.fx import resize
21
+ from moviepy.editor import VideoFileClip, CompositeVideoClip, ColorClip, ImageClip, TextClip
22
+ from PIL import Image, ImageDraw, ImageFont
23
+ from datetime import datetime, timedelta, timezone
24
+ from moviepy.editor import *
25
+ import builtins
26
+ import logging
27
+ import re
28
+
29
+ import builtins
30
+ import re
31
+ import subprocess
32
+
33
+
34
+ #====== LOGGING SETUP ======
35
+ logging.basicConfig(
36
+ level=logging.INFO,
37
+ format="%(asctime)s - %(levelname)s - %(message)s",
38
+ handlers=[
39
+ logging.FileHandler("app.log"),
40
+ logging.StreamHandler()
41
+ ]
42
+ )
43
+ logger = logging.getLogger(__name__)
44
+
45
+ # ====== FLASK APP ======
46
+ app = Flask(__name__)
47
+
48
+ # ====== MONGO DB ======
49
+ client = MongoClient(os.getenv("MONGO_URI", "mongodb+srv://kanhagarg930123:[email protected]/?retryWrites=true&w=majority&appName=kanha"))
50
+ db1 = client.shortttt
51
+ meta = db1.meta
52
+ db2 = client["instagram_bot"]
53
+ reel_progress = db2["reel_progresss"]
54
+ # ====== CONSTANTS ======
55
+ CAPTIONS = [
56
+ "Wait for it 😜", "Watch till end πŸ˜‚", "Try not to laugh 🀣",
57
+ "Don't skip this πŸ”₯", "You won't expect this! πŸ˜€", "Keep watching πŸ˜†",
58
+ "Stay till end! πŸ’₯", "Funniest one yet"
59
+ ]
60
+
61
+ BLOCKLIST = [
62
+ "nsfw","18+", "xxx", "sexy", "adult", "porn", "onlyfans", "escort",
63
+ "betting", "gambling", "iplwin", "1xbet", "winzo", "my11circle", "dream11",
64
+ "rummy", "teenpatti", "fantasy", "casino", "promotion"
65
+ ]
66
+
67
+ UPLOAD_TIMES = []
68
+ NEXT_RESET = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
69
+
70
+ # ====== FUNCTIONS ======
71
+ def get_next_part():
72
+ last = meta.find_one(sort=[("part", -1)])
73
+ return 1 if not last else last["part"] + 1
74
+
75
+ def generate_description(title):
76
+ return f"Watch this hilarious clip: {title}"
77
+
78
+ # MongoDB integration assumed via reel_progress collection
79
+ def get_next_fetch_index(username):
80
+ entry = reel_progress.find_one({"username": username})
81
+ return entry["last_index"] if entry and "last_index" in entry else 0
82
+
83
+ def update_fetch_index(username, new_index):
84
+ reel_progress.update_one(
85
+ {"username": username},
86
+ {"$set": {"last_index": new_index}},
87
+ upsert=True
88
+ )
89
+
90
+ def is_reel_fetched_or_skipped(username, reel_id):
91
+ entry = reel_progress.find_one({"username": username})
92
+ if not entry:
93
+ return False
94
+ return reel_id in entry.get("fetched_ids", []) or reel_id in entry.get("skipped_ids", [])
95
+
96
+ def mark_reel_fetched(username, reel_id):
97
+ reel_progress.update_one(
98
+ {"username": username},
99
+ {"$addToSet": {"fetched_ids": reel_id}},
100
+ upsert=True
101
+ )
102
+
103
+ def mark_reel_skipped(username, reel_id):
104
+ reel_progress.update_one(
105
+ {"username": username},
106
+ {"$addToSet": {"skipped_ids": reel_id}},
107
+ upsert=True
108
+ )
109
+
110
+ import os
111
+ import base64
112
+ import tempfile
113
+ from telethon.sync import TelegramClient
114
+
115
+ import os
116
+ from telethon.sync import TelegramClient
117
+ from telethon.sessions import StringSession
118
+
119
+ TG_API_ID = int(os.getenv("API_ID", 3704772))
120
+ TG_API_HASH = os.getenv("API_HASH", "b8e50a035abb851c0dd424e14cac4c06")
121
+ SESSION_B64 = os.getenv("SESSION", "1BVtsOIEBu5jNa-E88vbC710wzyAKnteiQK7WQNvDK6wCeD3Q_c33uSaqvlvCUMglbhuVtugstOwIuE7WV2EuLGUEHH3AFJ84MZ93WVs010bx4N9nQOaQJNjIZDe4Nllq9r5PKRXxjgwuSqN-B7_TfpjQJT_ztOqQNZTQV3o9EqPBXfpiMzVF5U638wuRDVkInjbgAkI9ao36KDmcvBJzAW91l27loIsUL-Zst9H6XbAbVgqfs1fTOI6xQqPEyrFA-gB7lHbKrkqILwAmK88tTRQFHOvvXkGkJpDrXb1MZQssaVilGfXQ7sXWPltMpHklRy8HaPdfOUJ8t105DHXf-CN6SvzombM=")
122
+
123
+ if not SESSION_B64:
124
+ raise ValueError("SESSION is not set")
125
+
126
+ client = TelegramClient(StringSession(SESSION_B64), TG_API_ID, TG_API_HASH)
127
+
128
+ import json
129
+ import time
130
+ import random
131
+ import pathlib
132
+ import requests
133
+ from typing import Optional, Tuple
134
+ from telethon.sync import TelegramClient
135
+ from telethon.tl.functions.messages import GetHistoryRequest
136
+
137
+ # Assumes MongoDB functions are available:
138
+ # get_next_fetch_index, update_fetch_index, is_reel_fetched_or_skipped,
139
+ # mark_reel_fetched, mark_reel_skipped
140
+
141
+ RAW_USERNAMES = os.getenv("RAW_USERNAMES", "nickyteja06,videshi__indian,_notyourtype_yt_,sisterfunnyreel").split(",")
142
+ REACTED_USERNAMES = os.getenv("REACTED_USERNAMES", "biggestreact").split(",")
143
+
144
+
145
+ import os
146
+ import json
147
+ import time
148
+ import random
149
+ import pathlib
150
+ import requests
151
+ from typing import Optional, Tuple
152
+ from telethon.tl.functions.messages import GetHistoryRequest
153
+
154
+ # Constants
155
+ IG_HEADERS = {
156
+ "User-Agent": "Mozilla/5.0",
157
+ # Optional: Uncomment if using sessionid
158
+ # "Cookie": f"sessionid={os.getenv('IG_SESSION_ID')}"
159
+ }
160
+
161
+ def get_user_id(username: str) -> Optional[str]:
162
+ try:
163
+ res = requests.get(
164
+ f"https://www.instagram.com/api/v1/users/web_profile_info/?username={username}",
165
+ headers=IG_HEADERS,
166
+ timeout=10
167
+ )
168
+ if res.status_code == 200:
169
+ user_id = res.json()["data"]["user"]["id"]
170
+ logger.info(f"[πŸ†”] Got user ID for @{username}: {user_id}")
171
+ return user_id
172
+ logger.warning(f"[⚠️] Failed to get user ID for @{username}, status {res.status_code}")
173
+ except Exception as e:
174
+ logger.warning(f"[❌] get_user_id({username}) error: {e}")
175
+ return None
176
+
177
+ def get_links(user_id: str, limit: int = 5) -> list[Tuple[str, str]]:
178
+ links = []
179
+ query_hash = "58b6785bea111c67129decbe6a448951"
180
+ end_cursor = ""
181
+
182
+ while len(links) < limit:
183
+ variables = {
184
+ "id": user_id,
185
+ "first": 12,
186
+ "after": end_cursor
187
+ }
188
+ params = {
189
+ "query_hash": query_hash,
190
+ "variables": json.dumps(variables)
191
+ }
192
+
193
+ try:
194
+ res = requests.get("https://www.instagram.com/graphql/query/", params=params, headers=IG_HEADERS, timeout=10)
195
+ if res.status_code != 200:
196
+ logger.warning(f"[⚠️] Failed to get links for user {user_id}, status {res.status_code}")
197
+ break
198
+
199
+ media = res.json()["data"]["user"]["edge_owner_to_timeline_media"]
200
+ for edge in media["edges"]:
201
+ reel_id = edge["node"]["id"]
202
+ shortcode = edge["node"]["shortcode"]
203
+ link = f"https://www.instagram.com/p/{shortcode}/"
204
+ links.append((reel_id, link))
205
+ if len(links) >= limit:
206
+ break
207
+
208
+ if not media["page_info"]["has_next_page"]:
209
+ break
210
+ end_cursor = media["page_info"]["end_cursor"]
211
+ except Exception as e:
212
+ logger.warning(f"[❌] get_links() error: {e}")
213
+ break
214
+ logger.info(f"[πŸ”—] Found {len(links)} links for user ID {user_id}")
215
+ return links
216
+
217
+ def send_to_bot_and_get_video(link: str) -> Optional[str]:
218
+ try:
219
+ entity = client.loop.run_until_complete(client.get_entity("instasavegrambot"))
220
+ client.loop.run_until_complete(client.send_message(entity, link))
221
+
222
+ for _ in range(15): # Wait up to 30s
223
+ time.sleep(2)
224
+ history = client.loop.run_until_complete(
225
+ client(GetHistoryRequest(
226
+ peer=entity,
227
+ limit=2,
228
+ offset_id=0,
229
+ offset_date=None,
230
+ add_offset=0,
231
+ max_id=0,
232
+ min_id=0,
233
+ hash=0
234
+ ))
235
+ )
236
+ for msg in history.messages:
237
+ logger.info(f"[πŸ“¨] Bot message: {msg.message or '[media]'}")
238
+ if msg.video and 20 <= msg.video.duration <= 180:
239
+ path = f"reels/{msg.id}.mp4"
240
+ pathlib.Path("reels").mkdir(exist_ok=True)
241
+ client.loop.run_until_complete(msg.download_media(file=path))
242
+ logger.info(f"[βœ…] Downloaded reel to {path}")
243
+ return path
244
+ except Exception as e:
245
+ logger.warning(f"[❌] send_to_bot_and_get_video error: {e}")
246
+ return None
247
+
248
+ def fetch_valid_reel() -> Tuple[Optional[str], Optional[str]]:
249
+ with client:
250
+ pools = [(RAW_USERNAMES, "raw"), (REACTED_USERNAMES, "reacted")]
251
+ pools = [(p, t) for p, t in pools if p]
252
+ if not pools:
253
+ logger.warning("[⚠️] No usernames in either RAW or REACTED pools.")
254
+ return None, None
255
+
256
+ random.shuffle(pools)
257
+
258
+ for usernames, reel_type in pools:
259
+ random.shuffle(usernames)
260
+ for username in usernames:
261
+ logger.info(f"[πŸ”Ž] Trying @{username} ({reel_type})")
262
+ uid = get_user_id(username)
263
+ if not uid:
264
+ logger.warning(f"[🚫] Skipping @{username}, failed to get user ID.")
265
+ continue
266
+
267
+ fetch_idx = get_next_fetch_index(username)
268
+ links = get_links(uid, limit=5)
269
+
270
+ for idx, (reel_id, link) in enumerate(links[fetch_idx:], start=fetch_idx):
271
+ if is_reel_fetched_or_skipped(username, reel_id):
272
+ logger.info(f"[⏩] Already processed: @{username}/{reel_id}")
273
+ continue
274
+
275
+ logger.info(f"[πŸ“₯] Sending link to bot: {link}")
276
+ video_path = send_to_bot_and_get_video(link)
277
+ if not video_path:
278
+ logger.warning(f"[⚠️] Failed to fetch: {link}")
279
+ mark_reel_skipped(username, reel_id)
280
+ continue
281
+
282
+ mark_reel_fetched(username, reel_id)
283
+ update_fetch_index(username, idx + 1)
284
+ return video_path, reel_type
285
+
286
+ update_fetch_index(username, fetch_idx + len(links))
287
+
288
+ logger.warning("[🚫] No valid reels found after checking all usernames.")
289
+ return None, None
290
+
291
+ def patch_moviepy():
292
+ original_resizer = resize.resize
293
+
294
+ def patched_resizer(clip, *args, **kwargs):
295
+ newsize = kwargs.get("newsize", None)
296
+ if newsize:
297
+ newsize = tuple(map(int, newsize))
298
+ clip = clip.fl_image(lambda img: img.resize(newsize, Image.Resampling.LANCZOS))
299
+ else:
300
+ clip = original_resizer(clip, *args, **kwargs)
301
+ return clip
302
+
303
+ resize.resize = patched_resizer
304
+
305
+ patch_moviepy()
306
+
307
+ import emoji
308
+ from PIL import Image, ImageDraw, ImageFont
309
+ import emoji
310
+ import os
311
+ import requests
312
+
313
+
314
+ def create_text_image(text, width, height):
315
+ img = Image.new("RGB", (width, height), color=(255, 255, 255))
316
+ draw = ImageDraw.Draw(img)
317
+
318
+ # Load font
319
+ try:
320
+ font = ImageFont.truetype("DejaVuSans-Bold.ttf", size=60)
321
+ except:
322
+ font = ImageFont.load_default()
323
+
324
+ # Extract emoji and clean text
325
+ emojis = emoji.emoji_list(text)
326
+ pure_text = emoji.replace_emoji(text, replace='')
327
+
328
+ # Adjust font size to fit
329
+ max_font_size = 70
330
+ while True:
331
+ font = ImageFont.truetype("DejaVuSans-Bold.ttf", size=max_font_size)
332
+ text_width, text_height = draw.textsize(pure_text, font=font)
333
+ total_width = text_width + (len(emojis) * 60) + 20
334
+ if total_width <= width - 40 or max_font_size <= 30:
335
+ break
336
+ max_font_size -= 2
337
+
338
+ # Starting X & Y for centered layout
339
+ start_x = (width - total_width) // 2
340
+ y = (height - text_height) // 2
341
+
342
+ # Draw text first
343
+ draw.text((start_x, y), pure_text, font=font, fill=(0, 0, 0))
344
+
345
+ # Then draw emojis to the right of the text
346
+ x = start_x + text_width + 10
347
+ for e in emojis:
348
+ hexcode = '-'.join(f"{ord(c):x}" for c in e['emoji'])
349
+ emoji_path = f"emoji_pngs/{hexcode}.png"
350
+ if not os.path.exists(emoji_path):
351
+ download_emoji_png(e['emoji'])
352
+
353
+ if os.path.exists(emoji_path):
354
+ emoji_img = Image.open(emoji_path).convert("RGBA")
355
+ emoji_img = emoji_img.resize((60, 60))
356
+ img.paste(emoji_img, (x, y), emoji_img)
357
+ x += 60 + 4
358
+
359
+ return img
360
+
361
+ from PIL import Image, ImageDraw, ImageFont
362
+ import numpy as np
363
+ from moviepy.editor import ImageClip
364
+
365
+ def generate_watermark_img(text, width, height=50):
366
+ img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
367
+ draw = ImageDraw.Draw(img)
368
+
369
+ try:
370
+ font = ImageFont.truetype("DejaVuSans-Bold.ttf", size=35)
371
+ except:
372
+ font = ImageFont.load_default()
373
+
374
+ text_width, text_height = draw.textsize(text, font=font)
375
+ draw.text((5, height - text_height - 2), text, fill="white", font=font, stroke_width=1, stroke_fill="black")
376
+
377
+ return img
378
+
379
+ def download_emoji_png(emoji_char):
380
+ hexcode = '-'.join(f"{ord(c):x}" for c in emoji_char)
381
+ url = f"https://github.com/twitter/twemoji/raw/master/assets/72x72/{hexcode}.png"
382
+ os.makedirs("emoji_pngs", exist_ok=True)
383
+ path = f"emoji_pngs/{hexcode}.png"
384
+ try:
385
+ r = requests.get(url)
386
+ if r.status_code == 200:
387
+ with open(path, "wb") as f:
388
+ f.write(r.content)
389
+ print(f"βœ… Downloaded emoji: {emoji_char} β†’ {path}")
390
+ else:
391
+ print(f"❌ Failed to download emoji: {emoji_char}")
392
+ except Exception as e:
393
+ print(f"⚠️ Error downloading emoji {emoji_char}: {e}")
394
+
395
+
396
+ def edit_video(video_path):
397
+ clip = VideoFileClip(video_path)
398
+
399
+ video_width = clip.w
400
+ video_height = clip.h
401
+ bar_height = 120
402
+ total_height = video_height + bar_height
403
+
404
+ # === 1. Background Canvas
405
+ final_bg = ColorClip(size=(video_width, total_height), color=(255, 255, 255), duration=clip.duration)
406
+
407
+ # === 2. Caption Bar (Top)
408
+ caption = random.choice(CAPTIONS)
409
+ caption_img = create_text_image(caption, video_width, bar_height)
410
+ caption_clip = ImageClip(np.array(caption_img)).set_duration(clip.duration).set_position((0, 0))
411
+
412
+ # === 3. Eye Protection Overlay (6% White Transparent)
413
+ eye_protection = ColorClip(size=(video_width, video_height), color=(255, 255, 255), duration=clip.duration)
414
+ eye_protection = eye_protection.set_opacity(0.1).set_position((0, bar_height))
415
+
416
+ # === 4. Watermark (Bottom-left using Pillow + ImageClip)
417
+ watermark_img = generate_watermark_img("@fulltosscomedy4u", video_width, height=50)
418
+ watermark_clip = ImageClip(np.array(watermark_img)).set_duration(clip.duration).set_position(("left", bar_height + video_height - 50))
419
+
420
+ # === 5. Position original video below top bar
421
+ video_clip = clip.set_position((0, bar_height))
422
+
423
+ # === 6. Combine everything
424
+ final = CompositeVideoClip(
425
+ [final_bg, caption_clip, video_clip, eye_protection, watermark_clip],
426
+ size=(video_width, total_height)
427
+ )
428
+
429
+ os.makedirs("edited", exist_ok=True)
430
+ output_path = f"edited/{uuid.uuid4().hex}.mp4"
431
+ final.write_videofile(
432
+ output_path,
433
+ codec="libx264",
434
+ audio_codec="aac",
435
+ preset="slow",
436
+ bitrate="12000k",
437
+ verbose=False,
438
+ logger=None
439
+ )
440
+ return output_path
441
+
442
+ def edit_video_raw(video_path):
443
+ clip = VideoFileClip(video_path)
444
+ video_width = clip.w
445
+ video_height = clip.h
446
+ top_bar_height = 120
447
+ mid_bar_height = 80
448
+ total_height = video_height + top_bar_height + mid_bar_height
449
+
450
+ # === 1. Background Canvas
451
+ final_bg = ColorClip(size=(video_width, total_height), color=(255, 255, 255), duration=clip.duration + 6)
452
+
453
+ # === 2. Top Caption (Black on White)
454
+ caption = random.choice(CAPTIONS)
455
+ caption_img = create_text_image(
456
+ caption,
457
+ width=video_width,
458
+ height=top_bar_height,
459
+ font_size=min(max(int(video_width * 0.08), 48), 80), # Auto-resize
460
+ align="center",
461
+ bg_color=(255, 255, 255),
462
+ text_color=(0, 0, 0)
463
+ )
464
+ caption_clip = ImageClip(np.array(caption_img)).set_duration(clip.duration + 6).set_position((0, 0))
465
+
466
+ # === 3. Middle Caption (White on Black)
467
+ mid_text = random.choice([
468
+ "Pura 1 din laga tab ye reel mili 🀣",
469
+ "Ye miss mat kr dena 😜",
470
+ "Kha thi ye reel ab tak πŸ€¨πŸ€—",
471
+ "Wait, ye dekh kr hi janna πŸ’₯πŸ’₯"
472
+ ])
473
+ mid_img = create_text_image(
474
+ mid_text,
475
+ width=video_width,
476
+ height=mid_bar_height,
477
+ font_size=min(max(int(video_width * 0.06), 40), 64),
478
+ align="center",
479
+ bg_color=(0, 0, 0),
480
+ text_color=(255, 255, 255)
481
+ )
482
+ mid_caption_clip = ImageClip(np.array(mid_img)).set_duration(clip.duration + 6).set_position((0, top_bar_height))
483
+
484
+ # === 4. Eye Protection Overlay
485
+ eye_protection = ColorClip(size=(video_width, video_height), color=(255, 255, 255), duration=clip.duration + 6)
486
+ eye_protection = eye_protection.set_opacity(0.1).set_position((0, top_bar_height + mid_bar_height))
487
+
488
+ # === 5. Watermark (Bottom Left)
489
+ watermark_img = generate_watermark_img("@fulltosscomedy4u", video_width, height=50)
490
+ watermark_clip = ImageClip(np.array(watermark_img)).set_duration(clip.duration + 6).set_position(("left", total_height - 50))
491
+
492
+ # === 6. Meme Section: Laugh -> Freeze -> Laugh
493
+ laugh_index = random.choice([1, 2])
494
+ laugh_clip = VideoFileClip(f"laugh/{laugh_index}.mp4").resize(width=video_width).set_duration(2)
495
+ freeze_frame = laugh_clip.to_ImageClip().set_duration(max(1, clip.duration - 4))
496
+ meme_part = concatenate_videoclips([laugh_clip, freeze_frame, laugh_clip])
497
+
498
+ # === 7. Combine All Video Parts
499
+ full_video = concatenate_videoclips([meme_part, clip])
500
+ full_video = full_video.set_position((0, top_bar_height + mid_bar_height))
501
+
502
+ # === 8. Composite Final Video
503
+ final = CompositeVideoClip(
504
+ [final_bg, caption_clip, mid_caption_clip, full_video, eye_protection, watermark_clip],
505
+ size=(video_width, total_height)
506
+ )
507
+
508
+ # === 9. Export
509
+ os.makedirs("edited", exist_ok=True)
510
+ output_path = f"edited/{uuid.uuid4().hex}.mp4"
511
+ final.write_videofile(
512
+ output_path,
513
+ codec="libx264",
514
+ audio_codec="aac",
515
+ preset="slow",
516
+ bitrate="12000k",
517
+ threads=4,
518
+ verbose=False,
519
+ logger=None
520
+ )
521
+ return output_path
522
+
523
+ def upload_to_youtube(video_path, title, desc):
524
+ creds = Credentials(
525
+ token=None,
526
+ refresh_token=os.getenv("YT_REFRESH_TOKEN"),
527
+ token_uri="https://oauth2.googleapis.com/token",
528
+ client_id=os.getenv("YT_CLIENT_ID", "387804137131-sjih05p3329n5n72gsgglv1tb9t62882.apps.googleusercontent.com"),
529
+ client_secret=os.getenv("YT_CLIENT_SECRET", "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"),
530
+ scopes=["https://www.googleapis.com/auth/youtube.upload"]
531
+ )
532
+ creds.refresh(Request())
533
+ youtube = build("youtube", "v3", credentials=creds)
534
+ request = youtube.videos().insert(
535
+ part="snippet,status",
536
+ body={
537
+ "snippet": {
538
+ "title": title,
539
+ "description": desc,
540
+ "tags": ["funny", "memes", "comedy", "shorts"],
541
+ "categoryId": "23"
542
+ },
543
+ "status": {
544
+ "privacyStatus": "public",
545
+ "madeForKids": False
546
+ }
547
+ },
548
+ media_body=MediaFileUpload(video_path)
549
+ )
550
+ res = request.execute()
551
+ logger.info(f"Uploaded: https://youtube.com/watch?v={res['id']}")
552
+ return f"https://youtube.com/watch?v={res['id']}"
553
+
554
+ first_run = True
555
+ def save_to_db(part, title, desc, link):
556
+ meta.insert_one({"part": part, "title": title, "description": desc, "link": link, "uploaded": time.time()})
557
+
558
+ def auto_loop():
559
+ asyncio.set_event_loop(asyncio.new_event_loop())
560
+ global UPLOAD_TIMES, NEXT_RESET, first_run
561
+
562
+ ist = timezone(timedelta(hours=5, minutes=30)) # IST timezone
563
+
564
+ daily_upload_count = random.randint(3, 5)
565
+ uploads_done_today = 0
566
+ NEXT_RESET = datetime.now(ist).replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
567
+
568
+ logger.info(f"[πŸ“…] Today's upload target: {daily_upload_count} reels.")
569
+
570
+ def wait_until(hour: int, minute: int = 0):
571
+ now = datetime.now(ist)
572
+ target = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
573
+ if target < now:
574
+ return
575
+ logger.info(f"[πŸ•—] Waiting until {target.strftime('%H:%M')} IST...")
576
+ while datetime.now(ist) < target:
577
+ time.sleep(10)
578
+
579
+ wait_until(8)
580
+
581
+ while True:
582
+ try:
583
+ now = datetime.now(ist)
584
+
585
+ if now >= NEXT_RESET:
586
+ UPLOAD_TIMES.clear()
587
+ NEXT_RESET = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
588
+ daily_upload_count = random.randint(3, 5)
589
+ uploads_done_today = 0
590
+ logger.info(f"[πŸ”] Reset for new day. New target: {daily_upload_count} uploads.")
591
+
592
+ if uploads_done_today >= daily_upload_count:
593
+ logger.info("[βœ…] Daily upload target reached.")
594
+ time.sleep(60)
595
+ continue
596
+
597
+ if now.hour < 8 or now.hour >= 20:
598
+ time.sleep(60)
599
+ continue
600
+
601
+ if not UPLOAD_TIMES or (now - UPLOAD_TIMES[-1]).total_seconds() >= random.randint(7200, 14400):
602
+ video_path, reel_type = fetch_valid_reel()
603
+
604
+ if not video_path:
605
+ logger.warning("[⚠️] No valid reel found. Retrying...")
606
+ time.sleep(60)
607
+ continue
608
+ if reel_type == "reacted":
609
+ edited = edit_video(video_path)
610
+ else:
611
+ edited = edit_video_raw(video_path)
612
+ part = get_next_part()
613
+ title = f"Try not to laugh || #{part} #funny #memes #comedy #shorts"
614
+ desc = generate_description(title)
615
+ link = upload_to_youtube(edited, title, desc)
616
+ save_to_db(part, title, desc, link)
617
+
618
+ logger.info(f"[πŸ“€] Uploaded #{part}: {link}")
619
+ UPLOAD_TIMES.append(now)
620
+ uploads_done_today += 1
621
+
622
+ os.remove(video)
623
+ os.remove(edited)
624
+
625
+ if uploads_done_today < daily_upload_count:
626
+ gap_seconds = random.randint(7200, 14400)
627
+ next_time = datetime.now(ist) + timedelta(seconds=gap_seconds)
628
+ if next_time.hour >= 20:
629
+ logger.info("[πŸŒ™] Next upload would exceed 8PM. Skipping.")
630
+ continue
631
+ logger.info(f"[⏳] Waiting ~{gap_seconds // 60} minutes before next upload.")
632
+ time.sleep(gap_seconds)
633
+ else:
634
+ time.sleep(60)
635
+
636
+ except Exception as e:
637
+ logger.error(f"Loop error: {e}")
638
+ time.sleep(60)
639
+
640
+ @app.route('/')
641
+ def home():
642
+ last = meta.find_one(sort=[("uploaded", -1)])
643
+ video_id = last["link"].split("v=")[-1] if last and "link" in last else None
644
+ return render_template("index.html", time=time.ctime(), video_id=video_id)
645
+
646
+ @app.route('/run-now', methods=["POST"])
647
+ def run_now():
648
+ Thread(target=auto_loop, daemon=True).start()
649
+ return redirect("/")
650
+
651
+ if __name__ == "__main__":
652
+ Thread(target=auto_loop, daemon=True).start()
653
+ app.run(host="0.0.0.0", port=7860)