···11+#!/usr/bin/env python3
22+"""
33+MIT License
44+55+Copyright (c) 2025
66+77+Permission is hereby granted, free of charge, to any person obtaining a copy
88+of this software and associated documentation files (the "Software"), to deal
99+in the Software without restriction, including without limitation the rights
1010+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1111+copies of the Software, and to permit persons to whom the Software is
1212+furnished to do so, subject to the following conditions:
1313+1414+The above copyright notice and this permission notice shall be included in all
1515+copies or substantial portions of the Software.
1616+1717+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1818+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1919+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2020+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2121+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2222+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2323+SOFTWARE.
2424+2525+---
2626+2727+INDEX:
2828+ 1. Load environment variables ...................... Line 56
2929+ 2. Parse command line arguments .................... Line 59
3030+ 3. Format dates .................................... Line 73
3131+ 4. Fetch bookmarks from Karakeep API ............... Line 78
3232+ 5. Save raw response ............................... Line 84
3333+ 6. Extract bookmarks ............................... Line 87
3434+ 7. Filter bookmarks by date range .................. Line 90
3535+ 8. Extract URLs from filtered bookmarks ............ Line 96
3636+ 9. Create downloads directory ...................... Line 106
3737+ 10. Download videos using yt-dlp .................... Line 109
3838+ 11. Filter videos by duration (max 3 min) ........... Line 118
3939+ 12. Create file list for ffmpeg ..................... Line 137
4040+ 13. Create compilation directory .................... Line 140
4141+ 14. Compile videos using ffmpeg ..................... Line 143
4242+ 15. Generate bookmark report ........................ Line 163
4343+"""
4444+4545+import os
4646+import re
4747+import subprocess
4848+import requests
4949+import argparse
5050+from datetime import datetime, timedelta
5151+from pathlib import Path
5252+from dotenv import load_dotenv
5353+from jinja2 import Template
5454+5555+# Load environment variables
5656+load_dotenv()
5757+5858+# Parse command line arguments
5959+parser = argparse.ArgumentParser()
6060+parser.add_argument(
6161+ '--start-date',
6262+ type=str,
6363+ default=(datetime.now() - timedelta(days=7)).isoformat()
6464+)
6565+parser.add_argument(
6666+ '--end-date',
6767+ type=str,
6868+ default=datetime.now().isoformat()
6969+)
7070+args = parser.parse_args()
7171+7272+# Format dates
7373+formatted_end_date = datetime.fromisoformat(args.end_date).strftime('%Y_%m_%d')
7474+start_ts = datetime.fromisoformat(args.start_date).timestamp() * 1000
7575+end_ts = datetime.fromisoformat(args.end_date).timestamp() * 1000
7676+7777+# Fetch bookmarks from Karakeep API
7878+response = requests.get(
7979+ f"{os.getenv('KARAKEEP_BASE_URL')}/api/v1/lists/{os.getenv('KARAKEEP_LIST_ID')}/bookmarks",
8080+ headers={'Authorization': f"Bearer {os.getenv('KARAKEEP_API_KEY')}"}
8181+).json()
8282+8383+# Save raw response
8484+Path('karakeep_response.json').write_text(__import__('json').dumps(response, indent=2))
8585+8686+# Extract bookmarks
8787+bookmarks = response.get('bookmarks', [])
8888+8989+# Filter bookmarks by date range
9090+filtered_bookmarks = [
9191+ b for b in bookmarks
9292+ if start_ts <= datetime.fromisoformat(b['createdAt'].replace('Z', '+00:00')).timestamp() * 1000 <= end_ts
9393+]
9494+9595+# Extract URLs from filtered bookmarks
9696+urls = [
9797+ u for b in bookmarks
9898+ if start_ts <= datetime.fromisoformat(b['createdAt'].replace('Z', '+00:00')).timestamp() * 1000 <= end_ts
9999+ for u in re.findall(
100100+ r'https?://\S+',
101101+ b.get('content', {}).get('url', '') + ' ' + (b.get('title') or '')
102102+ )
103103+]
104104+105105+# Create downloads directory
106106+Path(f'downloads/{formatted_end_date}').mkdir(parents=True, exist_ok=True)
107107+108108+# Download videos using yt-dlp
109109+for url in urls:
110110+ subprocess.run([
111111+ 'yt-dlp',
112112+ '--cookies-from-browser', 'firefox',
113113+ '-o', f'downloads/{formatted_end_date}/%(id)s.%(ext)s',
114114+ url
115115+ ])
116116+117117+# Get all downloaded files and filter by duration (max 3 minutes)
118118+all_files = sorted(Path(f'downloads/{formatted_end_date}').glob('*'))
119119+files = []
120120+for f in all_files:
121121+ result = subprocess.run([
122122+ 'ffprobe',
123123+ '-v', 'error',
124124+ '-show_entries', 'format=duration',
125125+ '-of', 'default=noprint_wrappers=1:nokey=1',
126126+ str(f)
127127+ ], capture_output=True, text=True)
128128+ try:
129129+ duration = float(result.stdout.strip())
130130+ if duration <= 180: # 3 minutes = 180 seconds
131131+ files.append(f)
132132+ else:
133133+ f.unlink() # Delete videos longer than 3 minutes
134134+ except (ValueError, AttributeError):
135135+ files.append(f) # Keep if duration can't be determined
136136+137137+# Create file list for ffmpeg
138138+Path('filelist.txt').write_text('\n'.join(
139139+ f"file '{str(f.resolve()).replace(chr(39), chr(39)+chr(92)+chr(39)+chr(39))}'"
140140+ for f in files
141141+))
142142+143143+# Create compilation directory
144144+Path('compilation').mkdir(exist_ok=True)
145145+146146+# Compile videos using ffmpeg
147147+subprocess.run([
148148+ 'ffmpeg',
149149+ '-f', 'concat',
150150+ '-safe', '0',
151151+ '-i', 'filelist.txt',
152152+ '-c:v', 'libx264',
153153+ '-preset', 'fast',
154154+ '-crf', '23',
155155+ '-c:a', 'aac',
156156+ '-b:a', '128k',
157157+ '-r', '30',
158158+ '-g', '30',
159159+ '-avoid_negative_ts', 'make_zero',
160160+ '-fflags', '+genpts',
161161+ '-movflags', '+faststart',
162162+ '-y',
163163+ f'compilation/{formatted_end_date}.mp4'
164164+])
165165+166166+# Generate bookmark report
167167+template = Template(Path('templates/bookmark_report.md.j2').read_text())
168168+report = template.render(
169169+ start_date=args.start_date,
170170+ end_date=args.end_date,
171171+ bookmarks=filtered_bookmarks
172172+)
173173+Path(f'compilation/{formatted_end_date}.md').write_text(report)