diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-04-16 23:55:00 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-04-16 23:55:00 -0700 |
| commit | a5b15484423f9c9c9518a7be49845f018a8ff46f (patch) | |
| tree | 4d9254e50f62d2cfc5c5269950d08b0cb9b7dab4 | |
| parent | f140219f082e658f65a10d9ebfa070f5d649771d (diff) | |
feat: add support for DDR World
| -rw-r--r-- | bemani/ddr.py | 56 | ||||
| -rw-r--r-- | bemani/iidx.py | 3 | ||||
| -rw-r--r-- | bemani/sdvx.py | 3 | ||||
| -rw-r--r-- | constants.py | 2 | ||||
| -rw-r--r-- | generate.py | 5 | ||||
| -rw-r--r-- | news_feed.py | 12 | ||||
| -rw-r--r-- | site/src/components/TitleBar.tsx | 221 | ||||
| -rw-r--r-- | site/src/utils.ts | 1 |
8 files changed, 196 insertions, 107 deletions
diff --git a/bemani/ddr.py b/bemani/ddr.py new file mode 100644 index 0000000..947728c --- /dev/null +++ b/bemani/ddr.py @@ -0,0 +1,56 @@ +from bs4 import BeautifulSoup +from datetime import datetime +from urllib.parse import urljoin +import time +import re + +def parse_ddr_world_news_site(html: str): + base_url = "https://p.eagate.573.jp" + soup = BeautifulSoup(html, 'html.parser') + news_entries = [] + + for div in soup.select("div#info > div.news_one"): + if 'none' in div.get('style', ''): + continue + title_tag = div.select_one("div.news_title > div.title") + date_tag = div.select_one("div.news_title > div.date") + headline = title_tag.get_text(strip=True) if title_tag else None + date_str = date_tag.get_text(strip=True) if date_tag else None + + try: + dt = datetime.strptime(date_str, "%Y/%m/%d") + date_iso = dt.strftime("%Y-%m-%d") + timestamp = int(time.mktime(dt.timetuple())) + except Exception: + date_iso, timestamp = None, None + + paras = [p.get_text(strip=True, separator="\n") + for p in div.find_all("p", recursive=False)] + if not paras: + for child in div.find_all(recursive=False): + cls = child.get("class", []) + if "news_title" in cls or "img_news_center" in cls: + continue + if child.name == "div": + paras.append(child.get_text(strip=True, separator="\n")) + content = "\n\n".join(paras) if paras else None + + # image (use data-src if present) + img = div.select_one("div.img_news_center img") + raw_src = img.get("data-src") or img.get("src") if img else None + image_url = urljoin(base_url, raw_src) if raw_src else None + + news_entries.append({ + "date": date_iso, + "identifier": "DDR_WORLD", + "type": None, + "timestamp": timestamp, + "headline": headline, + "content": content, + "url": base_url, + "images": { + "image": image_url, + "link": None + } + }) + return news_entries diff --git a/bemani/iidx.py b/bemani/iidx.py index c13e05f..cc69fe1 100644 --- a/bemani/iidx.py +++ b/bemani/iidx.py @@ -7,7 +7,8 @@ KEY_TERMS_TL = [ ("クプロ", "QPro") ] -def parse_pinky_crush_news_site(html: str, base_url): +def parse_pinky_crush_news_site(html: str): + base_url = "https://p.eagate.573.jp" type_map = { "i_01": "NEWSONG", "i_02": "RANKING", diff --git a/bemani/sdvx.py b/bemani/sdvx.py index a87fe44..c77f198 100644 --- a/bemani/sdvx.py +++ b/bemani/sdvx.py @@ -2,7 +2,8 @@ from bs4 import BeautifulSoup from datetime import datetime from urllib.parse import urljoin -def parse_exceed_gear_news_site(html: str, base_url: str): +def parse_exceed_gear_news_site(html: str): + base_url = "https://p.eagate.573.jp" soup = BeautifulSoup(html, 'html.parser') news_list = soup.select('.tab ul.news li') diff --git a/constants.py b/constants.py index 50f9166..ef92020 100644 --- a/constants.py +++ b/constants.py @@ -2,9 +2,9 @@ from enum import Enum DAYS_LIMIT=14 -EAMUSEMENT_BASE_URL = "https://p.eagate.573.jp" SOUND_VOLTEX_EXCEED_GEAR_NEWS_SITE ="https://p.eagate.573.jp/game/sdvx/vi/news/index.html" IIDX_PINKY_CRUSH_NEWS_SITE="https://p.eagate.573.jp/game/2dx/32/info/index.html" +DDR_WORLD_NEWS_SITE="https://p.eagate.573.jp/game/ddr/ddrworld/info/index.html" CHUNITHM_JP_NEWS_SITE="https://info-chunithm.sega.jp/" CHUNITHM_INTL_NEWS_SITE="https://info-chunithm.sega.com/" diff --git a/generate.py b/generate.py index e974bfa..46b689d 100644 --- a/generate.py +++ b/generate.py @@ -65,6 +65,9 @@ def generate_iidx_news_file(): def generate_sdvx_news_file(): return generate_news_file("sdvx_news", constants.SOUND_VOLTEX_EXCEED_GEAR_NEWS_SITE) +def generate_ddr_news_file(): + return generate_news_file("ddr_news", constants.DDR_WORLD_NEWS_SITE) + def generate_chunithm_jp_news_file(): return generate_news_file("chunithm_jp_news", constants.CHUNITHM_JP_NEWS_SITE, constants.CHUNITHM_VERSION.VERSE) @@ -88,6 +91,7 @@ if __name__ == "__main__": iidx_news_data = generate_iidx_news_file() sdvx_news_data = generate_sdvx_news_file() + ddr_news_data = generate_ddr_news_file() chunithm_jp_news_data = generate_chunithm_jp_news_file() maimaidx_jp_news_data = generate_maimaidx_jp_news_file() ongeki_jp_news_data = generate_ongeki_jp_news_file() @@ -97,6 +101,7 @@ if __name__ == "__main__": news = create_merged_feed( iidx_news_data, sdvx_news_data, + ddr_news_data, chunithm_jp_news_data, maimaidx_jp_news_data, ongeki_jp_news_data, diff --git a/news_feed.py b/news_feed.py index ac90f0f..75a3678 100644 --- a/news_feed.py +++ b/news_feed.py @@ -19,6 +19,7 @@ Generic format for a news entry. All keys are considered to be nullable from site_scraper import SiteScraper, download_site_as_html import bemani.sdvx as sound_voltex import bemani.iidx as iidx +import bemani.ddr as ddr import sega.chuni_jp as chunithm_jp import sega.chuni_intl as chuni_intl import sega.maimaidx_jp as maimaidx_jp @@ -30,14 +31,21 @@ import translate def get_news(news_url: str, version=None) -> list: if news_url == constants.SOUND_VOLTEX_EXCEED_GEAR_NEWS_SITE: site_data = download_site_as_html(news_url) - news_posts = sorted(sound_voltex.parse_exceed_gear_news_site(site_data, constants.EAMUSEMENT_BASE_URL), key=lambda x: x['timestamp'], reverse=True) + news_posts = sorted(sound_voltex.parse_exceed_gear_news_site(site_data), key=lambda x: x['timestamp'], reverse=True) news_posts = translate.add_translate_text_to_en(news_posts) elif news_url == constants.IIDX_PINKY_CRUSH_NEWS_SITE: site_data = download_site_as_html(news_url) - news_posts = sorted(iidx.parse_pinky_crush_news_site(site_data, constants.EAMUSEMENT_BASE_URL), key=lambda x: x['timestamp'], reverse=True) + news_posts = sorted(iidx.parse_pinky_crush_news_site(site_data), key=lambda x: x['timestamp'], reverse=True) news_posts = translate.add_translate_text_to_en(news_posts, iidx.KEY_TERMS_TL) + elif news_url == constants.DDR_WORLD_NEWS_SITE: + scraper = SiteScraper(headless=True) + site_data = scraper.get_page_source(news_url) + scraper.close() + news_posts = sorted(ddr.parse_ddr_world_news_site(site_data), key=lambda x: x['timestamp'], reverse=True) + news_posts = translate.add_translate_text_to_en(news_posts) + elif news_url == constants.CHUNITHM_JP_NEWS_SITE: site_data = download_site_as_html(news_url) if version == constants.CHUNITHM_VERSION.VERSE: diff --git a/site/src/components/TitleBar.tsx b/site/src/components/TitleBar.tsx index 8d4ca31..2f7e3c1 100644 --- a/site/src/components/TitleBar.tsx +++ b/site/src/components/TitleBar.tsx @@ -1,123 +1,140 @@ -import { Link } from 'react-router-dom'; -import { useState, useEffect, useRef } from 'react'; +import { Link } from "react-router-dom"; +import { useState, useEffect, useRef } from "react"; interface GameCategory { - name: string; - games: { id: string; title: string }[]; + name: string; + games: { id: string; title: string }[]; } const TitleBar: React.FC = () => { - const [dropdownOpen, setDropdownOpen] = useState(false); - const dropdownRef = useRef<HTMLDivElement>(null); + const [dropdownOpen, setDropdownOpen] = useState(false); + const dropdownRef = useRef<HTMLDivElement>(null); - const gameCategories: GameCategory[] = [ - { - name: "KONAMI", - games: [ - { id: "iidx", title: "beatmania IIDX" }, - { id: "sdvx", title: "SOUND VOLTEX" }, - ] - }, - { - name: "SEGA", - games: [ - { id: "chunithm_jp", title: "CHUNITHM (JAPAN)" }, - { id: "chunithm_intl", title: "CHUNITHM (INTERNATIONAL)" }, - { id: "maimaidx_jp", title: "maimai DX (JAPAN)" }, - { id: "maimaidx_intl", title: "maimai DX (INTERNATIONAL)"}, - { id: "ongeki_jp", title: "O.N.G.E.K.I"}, - ] - } - ]; + const gameCategories: GameCategory[] = [ + { + name: "KONAMI", + games: [ + { id: "iidx", title: "beatmania IIDX" }, + { id: "sdvx", title: "SOUND VOLTEX" }, + { id: "ddr", title: "DanceDanceRevolution"} + ], + }, + { + name: "SEGA", + games: [ + { id: "chunithm_jp", title: "CHUNITHM (JAPAN)" }, + { id: "chunithm_intl", title: "CHUNITHM (INTL)" }, + { id: "maimaidx_jp", title: "maimai DX (JAPAN)" }, + { id: "maimaidx_intl", title: "maimai DX (INTL)" }, + { id: "ongeki_jp", title: "O.N.G.E.K.I" }, + ], + }, + ]; - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setDropdownOpen(false); - } - }; + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setDropdownOpen(false); + } + }; - if (dropdownOpen) { - document.addEventListener('mousedown', handleClickOutside); - } + if (dropdownOpen) { + document.addEventListener("mousedown", handleClickOutside); + } - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [dropdownOpen]); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [dropdownOpen]); - return ( - <div className="bg-gray-900 border-b border-gray-800 py-4 px-6"> - <div className="max-w-[800px] mx-auto"> - <div className="flex flex-col sm:flex-row justify-between items-center"> - <div className="flex items-center space-x-3 mb-3 sm:mb-0"> - <img - src="/rasis.webp" - alt="573 Updates Logo" - className="w-8 h-8 object-contain" - /> - <div className="w-8 h-8 bg-red-700 rounded-md flex items-center justify-center"> - <span className="text-white font-bold">573</span> - </div> + return ( + <div className="bg-gray-900 border-b border-gray-800 py-4 px-6"> + <div className="max-w-[800px] mx-auto"> + <div className="flex flex-col sm:flex-row justify-between items-center"> + <div className="flex items-center space-x-3 mb-3 sm:mb-0"> + <img + src="/rasis.webp" + alt="573 Updates Logo" + className="w-8 h-8 object-contain" + /> + <div className="w-8 h-8 bg-red-700 rounded-md flex items-center justify-center"> + <span className="text-white font-bold">573</span> + </div> - {/* Site Title */} - <Link to="/" className="text-xl font-bold text-white"> - UPDATES - </Link> - </div> + {/* Site Title */} + <Link to="/" className="text-xl font-bold text-white"> + UPDATES + </Link> + </div> - {/* Navigation Section */} - <div className="flex items-center space-x-4"> - <Link - to="/" - className="text-gray-300 hover:text-white font-medium" - > - All Games - </Link> + {/* Navigation Section */} + <div className="flex items-center space-x-4"> + <Link to="/" className="text-gray-300 hover:text-white font-medium"> + All Games + </Link> - {/* Dropdown Menu */} - <div className="relative" ref={dropdownRef}> - <button - className="text-gray-300 hover:text-white font-medium flex items-center" - onClick={() => setDropdownOpen(!dropdownOpen)} - > - Game Select - <svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> - </svg> - </button> + {/* Dropdown Menu */} + <div className="relative" ref={dropdownRef}> + <button + className="text-gray-300 hover:text-white font-medium flex items-center" + onClick={() => setDropdownOpen(!dropdownOpen)} + > + Game Select + <svg + className="w-4 h-4 ml-1" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M19 9l-7 7-7-7" + /> + </svg> + </button> - {dropdownOpen && ( - <div className="absolute right-0 mt-2 w-64 bg-gray-800 border border-gray-700 rounded-md shadow-lg z-10"> - <div className="py-1 max-h-[70vh] overflow-y-auto"> - {gameCategories.map((category, index) => ( - <div key={index} className="px-3 py-2"> - <div className="text-sm font-semibold text-gray-400 mb-1 border-b border-gray-700 pb-1"> - {category.name} - </div> - <div className="space-y-1"> - {category.games.map((game) => ( - <Link - key={game.id} - to={`/game/${game.id}`} - className="block text-gray-300 hover:bg-gray-700 hover:text-white px-2 py-1 text-sm rounded" - onClick={() => setDropdownOpen(false)} - > - {game.title} - </Link> - ))} - </div> - </div> - ))} - </div> - </div> - )} + {dropdownOpen && ( + <div className="absolute mt-2 w-64 sm:w-80 bg-gray-800 border border-gray-700 rounded-md shadow-lg z-10 right-0"> + <div className="py-1 max-h-[70vh] overflow-y-auto scroll-py-2"> + {gameCategories.map((category, index) => ( + <div key={index} className="px-2 py-1"> + <div className="text-sm font-semibold text-gray-400 mb-1 border-b border-gray-700 pb-1"> + {category.name} + </div> + <div + className={`${ + category.games.length > 3 + ? "grid grid-cols-1 sm:grid-cols-2 gap-x-2 gap-y-0.5" + : "space-y-0.5" + }`} + > + {category.games.map((game) => ( + <Link + key={game.id} + to={`/game/${game.id}`} + className="block text-left text-gray-300 hover:bg-gray-700 hover:text-white px-2 py-1 text-sm rounded whitespace-nowrap overflow-hidden text-ellipsis" + onClick={() => setDropdownOpen(false)} + > + {game.title} + </Link> + ))} </div> - </div> + </div> + ))} + </div> </div> + )} </div> + </div> </div> - ); + </div> + </div> + ); }; export default TitleBar; diff --git a/site/src/utils.ts b/site/src/utils.ts index 802d364..4984c58 100644 --- a/site/src/utils.ts +++ b/site/src/utils.ts @@ -10,6 +10,7 @@ export const getGameTitle = (gameId: string) => { if (lowerCaseGameId.startsWith("maimaidx_intl")) return "maimai DX (INTERNATIONAL)"; if (lowerCaseGameId.startsWith("ongeki_jp")) return "O.N.G.E.K.I" if (lowerCaseGameId.startsWith("chunithm_intl")) return "CHUNITHM (INTERNATIONAL)" + if (lowerCaseGameId.startsWIth("ddr")) return "DanceDanceRevolution" return gameId.toUpperCase(); }; |
