From a5b15484423f9c9c9518a7be49845f018a8ff46f Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Wed, 16 Apr 2025 23:55:00 -0700 Subject: feat: add support for DDR World --- bemani/ddr.py | 56 ++++++++++ bemani/iidx.py | 3 +- bemani/sdvx.py | 3 +- constants.py | 2 +- generate.py | 5 + news_feed.py | 12 ++- site/src/components/TitleBar.tsx | 221 +++++++++++++++++++++------------------ site/src/utils.ts | 1 + 8 files changed, 196 insertions(+), 107 deletions(-) create mode 100644 bemani/ddr.py 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(null); + const [dropdownOpen, setDropdownOpen] = useState(false); + const dropdownRef = useRef(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 ( -
-
-
-
- 573 Updates Logo -
- 573 -
+ return ( +
+
+
+
+ 573 Updates Logo +
+ 573 +
- {/* Site Title */} - - UPDATES - -
+ {/* Site Title */} + + UPDATES + +
- {/* Navigation Section */} -
- - All Games - + {/* Navigation Section */} +
+ + All Games + - {/* Dropdown Menu */} -
- + {/* Dropdown Menu */} +
+ - {dropdownOpen && ( -
-
- {gameCategories.map((category, index) => ( -
-
- {category.name} -
-
- {category.games.map((game) => ( - setDropdownOpen(false)} - > - {game.title} - - ))} -
-
- ))} -
-
- )} + {dropdownOpen && ( +
+
+ {gameCategories.map((category, index) => ( +
+
+ {category.name} +
+
3 + ? "grid grid-cols-1 sm:grid-cols-2 gap-x-2 gap-y-0.5" + : "space-y-0.5" + }`} + > + {category.games.map((game) => ( + setDropdownOpen(false)} + > + {game.title} + + ))}
-
+
+ ))} +
+ )}
+
- ); +
+
+ ); }; 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(); }; -- cgit v1.2.3