YouTube ロゴ再現
赤い角丸長方形の中に白い再生三角と右側に YouTube の文字
YouTube
福原弘基が作成しました
期間指定なし
過去1時間
今日
今週
今月
今年
カスタム
ホーム
急上昇
© 2023 福原弘基
API設定
YouTube Data API v3 キーを選択
再生パラメータソースを選択
キャンセル
保存して開始
日付範囲を指定
開始日
終了日
キャンセル
適用
設定
API設定を変更
現在の設定を変更します。変更後、現在の検索結果がリセットされる場合があります。
API設定を変更
テーマ
ライトモード
ダークモード
閉じる
import asyncio import json import time from datetime import datetime, timedelta from js import document, window, console, localStorage, fetch as js_fetch from pyodide.http import pyfetch import asyncio # アプリケーション状態 class AppState: def __init__(self): self.api_key = None self.param_source = None self.current_view = "home" self.search_query = "" self.current_channel = None self.next_page_token = None self.videos = [] self.current_video = None self.date_filter = "" self.custom_start_date = None self.custom_end_date = None self.theme = "light" self.param_sources = [ "https://raw.githubusercontent.com/siawaseok3/wakame/master/video_config.json", "https://raw.githubusercontent.com/wakame02/wktopu/refs/heads/main/edu.text", "https://raw.githubusercontent.com/woolisbest-4520/about-youtube/refs/heads/main/edu.json", "https://raw.githubusercontent.com/woolisbest-4520/about-youtube/refs/heads/main/parameter.json", "https://apis.kahoot.it/media-api/youtube/key" ] self.current_params = "" self.api_keys = [ "AIzaSyCz7f0X_giaGyC9u1EfGZPBuAC9nXiL5Mo", "AIzaSyBmzCw7-sX1vm-uL_u2Qy3LuVZuxye4Wys", "AIzaSyBWScla0K91jUL6qQErctN9N2b3j9ds7HI", "AIzaSyA17CdOQtQRC3DQe7rgIzFwTUjwAy_3CAc", "AIzaSyDdk_yY0tN4gKsm4uyMYrIlv1RwXIYXrnw", "AIzaSyDeU5zpcth2OgXDfToyc7-QnSJsDc41UGk", "AIzaSyClu2V_22XpCG2GTe1euD35_Mh5bn4eTjA" ] def save_to_storage(self): """状態をローカルストレージに保存""" data = { "api_key": self.api_key, "param_source": self.param_source, "theme": self.theme, "current_params": self.current_params } localStorage.setItem("youtube_clone_state", json.dumps(data)) def load_from_storage(self): """ローカルストレージから状態を読み込み""" data_str = localStorage.getItem("youtube_clone_state") if data_str: try: data = json.loads(data_str) self.api_key = data.get("api_key") self.param_source = data.get("param_source") self.theme = data.get("theme", "light") self.current_params = data.get("current_params", "") except: pass # メインアプリケーション class YouTubeClone: def __init__(self): self.state = AppState() self.state.load_from_storage() self.initialized = False async def init(self): """アプリケーションの初期化""" # DOM要素の取得 self.content_area = document.getElementById("content") self.api_modal = document.getElementById("apiModal") self.date_modal = document.getElementById("dateModal") self.settings_modal = document.getElementById("settingsModal") # イベントリスナーの設定 self.setup_event_listeners() # 初期状態の設定 document.body.setAttribute("data-theme", self.state.theme) self.update_theme_icon() # APIキーが設定されていない場合はモーダルを表示 if not self.state.api_key or not self.state.param_source: await self.show_api_modal() else: # ホーム動画を読み込み await self.load_home_videos() self.initialized = True def setup_event_listeners(self): """イベントリスナーを設定""" # 検索ボタン search_btn = document.getElementById("searchButton") search_input = document.getElementById("searchInput") async def on_search(e=None): query = search_input.value.strip() if query: await self.search_videos(query) search_btn.addEventListener("click", on_search) search_input.addEventListener("keypress", lambda e: asyncio.create_task(on_search()) if e.key == "Enter" else None) # ホームボタン home_btn = document.getElementById("homeBtn") home_btn.addEventListener("click", lambda e: asyncio.create_task(self.load_home_videos())) # 急上昇ボタン trending_btn = document.getElementById("trendingBtn") trending_btn.addEventListener("click", lambda e: asyncio.create_task(self.load_trending_videos())) # 日付フィルター date_filter = document.getElementById("dateFilter") date_filter.addEventListener("change", lambda e: asyncio.create_task(self.on_date_filter_change())) # テーマ切り替え theme_toggle = document.getElementById("themeToggle") theme_toggle.addEventListener("click", self.toggle_theme) # 設定ボタン settings_btn = document.getElementById("settingsButton") settings_btn.addEventListener("click", self.show_settings_modal) # APIモーダル関連 cancel_api_btn = document.getElementById("cancelApiBtn") cancel_api_btn.addEventListener("click", lambda: self.hide_modal(self.api_modal)) save_api_btn = document.getElementById("saveApiBtn") save_api_btn.addEventListener("click", lambda: asyncio.create_task(self.save_api_settings())) # 日付モーダル関連 cancel_date_btn = document.getElementById("cancelDateBtn") cancel_date_btn.addEventListener("click", lambda: self.hide_modal(self.date_modal)) apply_date_btn = document.getElementById("applyDateBtn") apply_date_btn.addEventListener("click", lambda: asyncio.create_task(self.apply_custom_date())) # 設定モーダル関連 close_settings_btn = document.getElementById("closeSettingsBtn") close_settings_btn.addEventListener("click", lambda: self.hide_modal(self.settings_modal)) change_api_btn = document.getElementById("changeApiBtn") change_api_btn.addEventListener("click", lambda: asyncio.create_task(self.show_api_modal())) light_theme_btn = document.getElementById("lightThemeBtn") light_theme_btn.addEventListener("click", lambda: self.set_theme("light")) dark_theme_btn = document.getElementById("darkThemeBtn") dark_theme_btn.addEventListener("click", lambda: self.set_theme("dark")) # ロゴクリック logo = document.getElementById("logo") logo.addEventListener("click", lambda e: asyncio.create_task(self.load_home_videos())) async def show_api_modal(self): """API設定モーダルを表示""" # APIキーオプションを生成 api_options = document.getElementById("apiKeyOptions") api_options.innerHTML = "" for i, key in enumerate(self.state.api_keys): option = document.createElement("div") option.className = "option-item" if key == self.state.api_key: option.classList.add("selected") option.setAttribute("data-index", str(i)) number = document.createElement("div") number.className = "option-number" number.textContent = str(i + 1) preview = document.createElement("div") preview.className = "option-preview" preview.textContent = key[:20] + "..." option.appendChild(number) option.appendChild(preview) def make_handler(idx): return lambda e: self.select_option(e, "api", idx) option.addEventListener("click", make_handler(i)) api_options.appendChild(option) # パラメータソースオプションを生成 param_options = document.getElementById("paramSourceOptions") param_options.innerHTML = "" for i in range(len(self.state.param_sources)): option = document.createElement("div") option.className = "option-item" if i == self.state.param_source: option.classList.add("selected") option.setAttribute("data-index", str(i)) number = document.createElement("div") number.className = "option-number" number.textContent = str(i + 1) preview = document.createElement("div") preview.className = "option-preview" preview.textContent = f"ソース {i + 1}" option.appendChild(number) option.appendChild(preview) def make_handler(idx): return lambda e: self.select_option(e, "param", idx) option.addEventListener("click", make_handler(i)) param_options.appendChild(option) # モーダルを表示 self.show_modal(self.api_modal) def select_option(self, event, type, index): """オプションを選択""" event.stopPropagation() if type == "api": options = document.querySelectorAll("#apiKeyOptions .option-item") for opt in options: opt.classList.remove("selected") event.currentTarget.classList.add("selected") else: options = document.querySelectorAll("#paramSourceOptions .option-item") for opt in options: opt.classList.remove("selected") event.currentTarget.classList.add("selected") async def save_api_settings(self): """API設定を保存""" # 選択されたAPIキーを取得 selected_api = document.querySelector("#apiKeyOptions .option-item.selected") if not selected_api: self.show_error("APIキーを選択してください") return api_index = int(selected_api.getAttribute("data-index")) self.state.api_key = self.state.api_keys[api_index] # 選択されたパラメータソースを取得 selected_param = document.querySelector("#paramSourceOptions .option-item.selected") if not selected_param: self.show_error("パラメータソースを選択してください") return param_index = int(selected_param.getAttribute("data-index")) self.state.param_source = param_index # パラメータを取得 await self.fetch_video_params() # 状態を保存 self.state.save_to_storage() # モーダルを閉じる self.hide_modal(self.api_modal) # ホーム動画を読み込み await self.load_home_videos() async def fetch_video_params(self): """動画再生パラメータを取得""" try: source_url = self.state.param_sources[self.state.param_source] # fetchを使用してデータを取得 response = await js_fetch(source_url) if not response.ok: raise Exception(f"HTTP error: {response.status}") if self.state.param_source == 0: # JSON形式 data = await response.json() self.state.current_params = data.get("params", "") elif self.state.param_source == 1: # テキスト形式 self.state.current_params = await response.text() elif self.state.param_source == 4: # JSON形式のキー data = await response.json() key = data.get("key", "") self.state.current_params = f"?key={key}" else: # その他の形式はパラメータなし self.state.current_params = "" console.log(f"取得したパラメータ: {self.state.current_params[:50]}...") except Exception as e: console.error(f"パラメータ取得エラー: {e}") # デフォルトパラメータ self.state.current_params = "?autoplay=1&controls=1&rel=0" async def load_home_videos(self): """ホーム動画を読み込み""" self.state.current_view = "home" self.state.search_query = "" self.state.current_channel = None self.state.next_page_token = None self.show_loading("動画を読み込んでいます...") try: # YouTube Data API v3を使用して人気動画を取得 url = f"https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&chart=mostPopular&maxResults=12®ionCode=JP&key={self.state.api_key}" response = await js_fetch(url) data = await response.json() if "error" in data: raise Exception(data["error"]["message"]) videos = data.get("items", []) self.state.videos = videos self.state.next_page_token = data.get("nextPageToken") # 動画を表示 await self.display_videos(videos) except Exception as e: self.show_error(f"動画の読み込みに失敗しました: {str(e)}") async def load_trending_videos(self): """急上昇動画を読み込み""" self.state.current_view = "trending" self.state.search_query = "" self.state.current_channel = None self.state.next_page_token = None self.show_loading("急上昇動画を読み込んでいます...") try: # より多くの結果を取得 url = f"https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&chart=mostPopular&maxResults=20®ionCode=JP&key={self.state.api_key}" response = await js_fetch(url) data = await response.json() if "error" in data: raise Exception(data["error"]["message"]) videos = data.get("items", []) self.state.videos = videos self.state.next_page_token = data.get("nextPageToken") # 動画を表示 await self.display_videos(videos) except Exception as e: self.show_error(f"急上昇動画の読み込みに失敗しました: {str(e)}") async def search_videos(self, query): """動画を検索""" self.state.current_view = "search" self.state.search_query = query self.state.current_channel = None self.state.next_page_token = None self.show_loading(f"「{query}」を検索中...") try: # 日付フィルターを適用 published_after = self.get_date_filter_param() url = f"https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&maxResults=12&q={query}&key={self.state.api_key}" if published_after: url += f"&publishedAfter={published_after}" response = await js_fetch(url) data = await response.json() if "error" in data: raise Exception(data["error"]["message"]) # 動画IDを収集 video_ids = [item["id"]["videoId"] for item in data.get("items", [])] if video_ids: # 動画の詳細情報を取得 details_url = f"https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&id={','.join(video_ids)}&key={self.state.api_key}" details_response = await js_fetch(details_url) details_data = await details_response.json() videos = details_data.get("items", []) else: videos = [] self.state.videos = videos self.state.next_page_token = data.get("nextPageToken") # 動画を表示 await self.display_videos(videos) except Exception as e: self.show_error(f"検索に失敗しました: {str(e)}") async def load_more_videos(self): """さらに動画を読み込み""" if not self.state.next_page_token: return self.show_loading("さらに読み込んでいます...") try: if self.state.current_view == "search": # 検索結果の続きを読み込み url = f"https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&maxResults=12&q={self.state.search_query}&pageToken={self.state.next_page_token}&key={self.state.api_key}" response = await js_fetch(url) data = await response.json() if "error" in data: raise Exception(data["error"]["message"]) # 動画IDを収集 video_ids = [item["id"]["videoId"] for item in data.get("items", [])] if video_ids: # 動画の詳細情報を取得 details_url = f"https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&id={','.join(video_ids)}&key={self.state.api_key}" details_response = await js_fetch(details_url) details_data = await details_response.json() new_videos = details_data.get("items", []) else: new_videos = [] self.state.videos.extend(new_videos) elif self.state.current_view == "home": # ホーム動画の続きを読み込み url = f"https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&chart=mostPopular&maxResults=12&pageToken={self.state.next_page_token}®ionCode=JP&key={self.state.api_key}" response = await js_fetch(url) data = await response.json() if "error" in data: raise Exception(data["error"]["message"]) new_videos = data.get("items", []) self.state.videos.extend(new_videos) self.state.next_page_token = data.get("nextPageToken") # 動画を表示 await self.display_videos(self.state.videos) except Exception as e: self.show_error(f"読み込みに失敗しました: {str(e)}") async def display_videos(self, videos): """動画を表示""" if not videos: self.show_empty_state("動画が見つかりませんでした") return # 動画グリッドを作成 grid = document.createElement("div") grid.className = "videos-grid" for video in videos: video_card = await self.create_video_card(video) grid.appendChild(video_card) # もっと読み込むボタン load_more_container = document.createElement("div") load_more_container.className = "load-more-container" if self.state.next_page_token: load_more_btn = document.createElement("button") load_more_btn.className = "load-more-button" load_more_btn.textContent = "もっと読み込む" load_more_btn.addEventListener("click", lambda e: asyncio.create_task(self.load_more_videos())) load_more_container.appendChild(load_more_btn) # コンテンツを更新 self.content_area.innerHTML = "" self.content_area.appendChild(grid) self.content_area.appendChild(load_more_container) async def create_video_card(self, video): """動画カードを作成""" snippet = video.get("snippet", {}) statistics = video.get("statistics", {}) content_details = video.get("contentDetails", {}) video_id = video.get("id") if "videoId" in video_id: video_id = video_id["videoId"] # カード要素 card = document.createElement("div") card.className = "video-card" card.setAttribute("data-video-id", video_id) # サムネイル thumbnail_url = snippet.get("thumbnails", {}).get("medium", {}).get("url", "") if not thumbnail_url and "thumbnails" in snippet: # 代替サムネイルを試す for size in ["high", "standard", "default"]: if size in snippet["thumbnails"]: thumbnail_url = snippet["thumbnails"][size]["url"] break # 動画時間のフォーマット duration = self.format_duration(content_details.get("duration", "")) # 視聴回数のフォーマット view_count = self.format_number(statistics.get("viewCount", 0)) # 公開日時のフォーマット published_at = self.format_date(snippet.get("publishedAt", "")) # カードのHTML card.innerHTML = f"""
{f'
{duration}
' if duration else ''}
{snippet.get('title', '')}
{snippet.get('channelTitle', '')}
{view_count} 回視聴
•
{published_at}
""" # イベントリスナー def on_card_click(e): if e.target.closest(".video-card-channel") or e.target.closest(".channel-avatar-small"): e.stopPropagation() channel_id = snippet.get("channelId") if channel_id: asyncio.create_task(self.load_channel(channel_id)) else: asyncio.create_task(self.play_video(video_id, video)) card.addEventListener("click", on_card_click) # チャンネルサムネイルを取得 channel_id = snippet.get("channelId") if channel_id: asyncio.create_task(self.load_channel_thumbnail(channel_id, card)) return card async def load_channel_thumbnail(self, channel_id, card): """チャンネルサムネイルを取得""" try: url = f"https://www.googleapis.com/youtube/v3/channels?part=snippet&id={channel_id}&key={self.state.api_key}" response = await js_fetch(url) data = await response.json() if "items" in data and data["items"]: thumbnail_url = data["items"][0]["snippet"]["thumbnails"]["default"]["url"] img = card.querySelector(".channel-avatar-small img") if img: img.src = thumbnail_url except: pass # サムネイルの取得に失敗しても無視 async def play_video(self, video_id, video_data=None): """動画を再生""" self.state.current_video = video_id if video_data is None: # 動画データを取得 try: url = f"https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&id={video_id}&key={self.state.api_key}" response = await js_fetch(url) data = await response.json() if "items" in data and data["items"]: video_data = data["items"][0] except: video_data = None # プレイヤーURLを構築 player_url = f"https://www.youtubeeducation.com/embed/{video_id}{self.state.current_params}" # プレイヤーと動画情報を表示 player_html = f"""
""" if video_data: snippet = video_data.get("snippet", {}) statistics = video_data.get("statistics", {}) # 視聴回数、高評価数などをフォーマット view_count = self.format_number(statistics.get("viewCount", 0)) like_count = self.format_number(statistics.get("likeCount", 0)) # 公開日時のフォーマット published_at = self.format_date(snippet.get("publishedAt", ""), full=True) info_html = f"""
{snippet.get('title', '')}
{view_count} 回視聴
•
{published_at}
説明
{snippet.get('description', '説明はありません')}
""" else: info_html = """
動画情報を読み込んでいます...
""" # 動画グリッドも表示 grid_html = "" if self.state.videos: grid = document.createElement("div") grid.className = "videos-grid" # 現在再生中の動画を除外 other_videos = [v for v in self.state.videos if v.get("id") != video_id and (isinstance(v.get("id"), str) and v.get("id") != video_id)] for video in other_videos[:12]: video_card = await self.create_video_card(video) grid.appendChild(video_card) grid_html = grid.outerHTML # コンテンツを更新 self.content_area.innerHTML = player_html + info_html + grid_html async def load_channel(self, channel_id): """チャンネルを読み込み""" self.state.current_view = "channel" self.state.current_channel = channel_id self.state.search_query = "" self.state.next_page_token = None self.show_loading("チャンネルを読み込んでいます...") try: # チャンネル情報を取得 channel_url = f"https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics&id={channel_id}&key={self.state.api_key}" channel_response = await js_fetch(channel_url) channel_data = await channel_response.json() if "error" in channel_data: raise Exception(channel_data["error"]["message"]) channel_info = channel_data.get("items", [{}])[0] # チャンネルの動画を取得 videos_url = f"https://www.googleapis.com/youtube/v3/search?part=snippet&channelId={channel_id}&type=video&maxResults=12&order=date&key={self.state.api_key}" videos_response = await js_fetch(videos_url) videos_data = await videos_response.json() if "error" in videos_data: raise Exception(videos_data["error"]["message"]) # 動画IDを収集 video_ids = [item["id"]["videoId"] for item in videos_data.get("items", [])] if video_ids: # 動画の詳細情報を取得 details_url = f"https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&id={','.join(video_ids)}&key={self.state.api_key}" details_response = await js_fetch(details_url) details_data = await details_response.json() videos = details_data.get("items", []) else: videos = [] self.state.videos = videos self.state.next_page_token = videos_data.get("nextPageToken") # チャンネルヘッダーを作成 snippet = channel_info.get("snippet", {}) statistics = channel_info.get("statistics", {}) subscriber_count = self.format_number(statistics.get("subscriberCount", 0)) video_count = self.format_number(statistics.get("videoCount", 0)) thumbnail_url = snippet.get("thumbnails", {}).get("medium", {}).get("url", "") channel_header = f"""
{snippet.get('title', '')}
{subscriber_count} 登録者
•
{video_count} 本の動画
{snippet.get('description', '')}
""" # 動画を表示 grid = document.createElement("div") grid.className = "videos-grid" for video in videos: video_card = await self.create_video_card(video) grid.appendChild(video_card) # もっと読み込むボタン load_more_container = document.createElement("div") load_more_container.className = "load-more-container" if self.state.next_page_token: load_more_btn = document.createElement("button") load_more_btn.className = "load-more-button" load_more_btn.textContent = "もっと読み込む" load_more_btn.addEventListener("click", lambda e: asyncio.create_task(self.load_more_channel_videos())) load_more_container.appendChild(load_more_btn) # コンテンツを更新 self.content_area.innerHTML = channel_header + grid.outerHTML + load_more_container.outerHTML except Exception as e: self.show_error(f"チャンネルの読み込みに失敗しました: {str(e)}") async def load_more_channel_videos(self): """チャンネル動画をさらに読み込み""" if not self.state.next_page_token or not self.state.current_channel: return self.show_loading("さらに読み込んでいます...") try: # チャンネル動画の続きを読み込み url = f"https://www.googleapis.com/youtube/v3/search?part=snippet&channelId={self.state.current_channel}&type=video&maxResults=12&order=date&pageToken={self.state.next_page_token}&key={self.state.api_key}" response = await js_fetch(url) data = await response.json() if "error" in data: raise Exception(data["error"]["message"]) # 動画IDを収集 video_ids = [item["id"]["videoId"] for item in data.get("items", [])] if video_ids: # 動画の詳細情報を取得 details_url = f"https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&id={','.join(video_ids)}&key={self.state.api_key}" details_response = await js_fetch(details_url) details_data = await details_response.json() new_videos = details_data.get("items", []) else: new_videos = [] self.state.videos.extend(new_videos) self.state.next_page_token = data.get("nextPageToken") # 動画を再表示 await self.display_channel_videos() except Exception as e: self.show_error(f"読み込みに失敗しました: {str(e)}") async def display_channel_videos(self): """チャンネル動画を表示""" if not self.state.videos: self.show_empty_state("このチャンネルに動画はありません") return # 動画グリッドを作成 grid = document.createElement("div") grid.className = "videos-grid" for video in self.state.videos: video_card = await self.create_video_card(video) grid.appendChild(video_card) # もっと読み込むボタン load_more_container = document.createElement("div") load_more_container.className = "load-more-container" if self.state.next_page_token: load_more_btn = document.createElement("button") load_more_btn.className = "load-more-button" load_more_btn.textContent = "もっと読み込む" load_more_btn.addEventListener("click", lambda e: asyncio.create_task(self.load_more_channel_videos())) load_more_container.appendChild(load_more_btn) # コンテンツを更新(チャンネルヘッダーはそのまま) existing_header = self.content_area.querySelector(".channel-header") if existing_header: self.content_area.innerHTML = existing_header.outerHTML + grid.outerHTML + load_more_container.outerHTML else: self.content_area.innerHTML = grid.outerHTML + load_more_container.outerHTML async def on_date_filter_change(self): """日付フィルター変更時の処理""" date_filter = document.getElementById("dateFilter") value = date_filter.value if value == "custom": self.show_modal(self.date_modal) else: self.state.date_filter = value self.state.custom_start_date = None self.state.custom_end_date = None # 検索中の場合は再検索 if self.state.current_view == "search" and self.state.search_query: await self.search_videos(self.state.search_query) async def apply_custom_date(self): """カスタム日付を適用""" start_date = document.getElementById("startDate").value end_date = document.getElementById("endDate").value if not start_date and not end_date: self.state.date_filter = "" self.state.custom_start_date = None self.state.custom_end_date = None else: self.state.date_filter = "custom" self.state.custom_start_date = start_date self.state.custom_end_date = end_date self.hide_modal(self.date_modal) # 日付フィルターを更新 date_filter = document.getElementById("dateFilter") date_filter.value = self.state.date_filter # 検索中の場合は再検索 if self.state.current_view == "search" and self.state.search_query: await self.search_videos(self.state.search_query) def get_date_filter_param(self): """日付フィルターパラメータを取得""" if self.state.date_filter == "custom": if self.state.custom_start_date: return self.state.custom_start_date + "T00:00:00Z" elif self.state.date_filter: now = datetime.now() if self.state.date_filter == "hour": delta = timedelta(hours=1) elif self.state.date_filter == "today": delta = timedelta(days=1) elif self.state.date_filter == "week": delta = timedelta(weeks=1) elif self.state.date_filter == "month": delta = timedelta(days=30) elif self.state.date_filter == "year": delta = timedelta(days=365) else: return None from_date = now - delta return from_date.isoformat() + "Z" return None def toggle_theme(self, event=None): """テーマを切り替え""" current_theme = document.body.getAttribute("data-theme") new_theme = "dark" if current_theme == "light" else "light" self.set_theme(new_theme) def set_theme(self, theme): """テーマを設定""" document.body.setAttribute("data-theme", theme) self.state.theme = theme self.state.save_to_storage() self.update_theme_icon() self.hide_modal(self.settings_modal) def update_theme_icon(self): """テーマアイコンを更新""" theme_toggle = document.getElementById("themeToggle") if theme_toggle: icon = theme_toggle.querySelector("i") if icon: if self.state.theme == "light": icon.className = "fas fa-moon" else: icon.className = "fas fa-sun" def show_settings_modal(self, event=None): """設定モーダルを表示""" self.show_modal(self.settings_modal) def show_modal(self, modal): """モーダルを表示""" modal.classList.add("active") def hide_modal(self, modal): """モーダルを非表示""" modal.classList.remove("active") def show_loading(self, message="読み込んでいます..."): """ローディング表示""" self.content_area.innerHTML = f"""
{message}
""" def show_error(self, message): """エラーを表示""" self.content_area.innerHTML = f"""
エラーが発生しました
{message}
再読み込み
""" def show_empty_state(self, message): """空の状態を表示""" self.content_area.innerHTML = f"""
{message}
別の検索キーワードをお試しください
""" def format_duration(self, duration): """動画時間をフォーマット""" if not duration: return "" # ISO 8601 期間フォーマットを処理 import re match = re.match(r'PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?', duration) if not match: return "" hours = int(match.group(1)) if match.group(1) else 0 minutes = int(match.group(2)) if match.group(2) else 0 seconds = int(match.group(3)) if match.group(3) else 0 if hours > 0: return f"{hours}:{minutes:02d}:{seconds:02d}" else: return f"{minutes}:{seconds:02d}" def format_number(self, num): """数値をフォーマット""" try: num = int(num) except: return "0" if num >= 1000000: return f"{num / 1000000:.1f}M".replace(".0", "") elif num >= 1000: return f"{num / 1000:.1f}K".replace(".0", "") else: return str(num) def format_date(self, date_str, full=False): """日付をフォーマット""" if not date_str: return "" try: # ISO 8601形式をパース from datetime import datetime if date_str.endswith('Z'): date_str = date_str[:-1] + '+00:00' dt = datetime.fromisoformat(date_str) now = datetime.now(dt.tzinfo) if dt.tzinfo else datetime.now() diff = now - dt if full: # 完全な日付形式 return dt.strftime("%Y年%m月%d日") else: # 相対時間 if diff.days == 0: return "今日" elif diff.days == 1: return "昨日" elif diff.days < 7: return f"{diff.days}日前" elif diff.days < 30: weeks = diff.days // 7 return f"{weeks}週間前" elif diff.days < 365: months = diff.days // 30 return f"{months}か月前" else: years = diff.days // 365 return f"{years}年前" except: return "" # アプリケーションのインスタンスを作成して初期化 app = YouTubeClone() # PyScriptが完全に読み込まれた後に初期化を実行 async def main(): await app.init() # メイン関数を実行 asyncio.create_task(main())