Skip to content

feat(desktop): YouTube URL から通知音を作成 (#258)#259

Merged
MocA-Love merged 2 commits into
mainfrom
feat/258-youtube-ringtone
Apr 17, 2026
Merged

feat(desktop): YouTube URL から通知音を作成 (#258)#259
MocA-Love merged 2 commits into
mainfrom
feat/258-youtube-ringtone

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

@MocA-Love MocA-Love commented Apr 17, 2026

概要

Settings > Notifications に From YouTube ボタンを追加し、YouTube の URL を貼って区間を指定するとカスタム通知音として取り込めるようにします。Issue #258 の対応です。

実装内容

  • apps/desktop/src/main/lib/youtube-ringtone.ts を新規追加。yt-dlp--download-sections で指定区間だけを mp3 として抽出し、既存の importCustomRingtoneFromPath に渡してカスタム通知音として登録。
  • apps/desktop/src/main/lib/custom-ringtones.tssetCustomRingtoneDisplayName を追加し、ユーザーが任意で付けたラベルをメタデータに保存できるように。
  • tRPC ルーター ringtone.importFromYouTube を追加(URL / 開始秒 / 長さ秒 / 任意の表示名)。
  • RingtonesSettings.tsxFrom YouTube ボタンとモーダルを追加。
  • YouTubeImportDialog を新規追加。URL / 開始時刻 (分・秒) / 長さ (最大30秒) / 表示名(任意)を入力。

動作要件

  • ローカルに yt-dlpffmpeg がインストールされていること(macOS なら brew install yt-dlp ffmpeg)。
  • 未インストール時はエラーメッセージで案内します。

テスト計画

  • Settings > Notifications で「From YouTube」ボタンが表示される
  • YouTube URL を入力し開始時刻 / 長さを指定して取り込みできる
  • 取り込んだクリップがプレビュー再生できる
  • yt-dlp / ffmpeg が無い環境でわかりやすいエラーが出る
  • 不正な URL で送信を弾く

Closes #258

Summary by CodeRabbit

リリースノート

  • New Features
    • YouTube動画からの通知音インポート機能を追加
    • 動画の特定区間(最大30秒)をクリップして通知音として設定可能に
    • カスタム通知音の表示名を設定・編集可能に

Settings > Notifications に "From YouTube" ボタンを追加し、
URL と開始時刻 / 長さ (最大30秒) を指定して
カスタム通知音をクリップ生成できるようにする。

ローカルにインストール済みの yt-dlp と ffmpeg を使用する。
未インストール時は分かりやすいエラーで案内する。

Closes #258
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Warning

Rate limit exceeded

@MocA-Love has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 38 minutes and 34 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 38 minutes and 34 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e74d3595-472f-4daf-806b-b3db27d624eb

📥 Commits

Reviewing files that changed from the base of the PR and between 088b2fa and 6742cc5.

📒 Files selected for processing (3)
  • apps/desktop/src/main/lib/custom-ringtones.ts
  • apps/desktop/src/main/lib/youtube-ringtone.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/RingtonesSettings/components/YouTubeImportDialog/YouTubeImportDialog.tsx
📝 Walkthrough

Walkthrough

YouTubeから通知音をインポートする機能を追加しました。フロントエンドダイアログで動画URLと時間範囲を入力し、バックエンドでyt-dlpffmpegを使用してオーディオを抽出・変換し、カスタム着信音として保存します。

Changes

Cohort / File(s) Summary
Backend - tRPC Router
apps/desktop/src/lib/trpc/routers/ringtone/index.ts
importFromYouTube公開ミューテーションを追加。YouTubeリンク、開始時間、期間、オプション表示名を入力として受け取り、エラーハンドリング(バイナリ欠損やタイムアウトはPRECONDITION_FAILED、その他はBAD_REQUEST)を実装。
Backend - YouTube Service
apps/desktop/src/main/lib/youtube-ringtone.ts
新規モジュール。yt-dlpffmpegの可用性確認、YouTube URLバリデーション、120秒タイムアウト付きダウンロード、MP3変換を実装。YouTubeRingtoneErrorクラスで型付きエラーコード(BINARY_MISSINGINVALID_URLINVALID_RANGEDOWNLOAD_FAILEDTIMEOUT)を定義。一時ディレクトリをクリーンアップ。
Backend - Custom Ringtone Metadata
apps/desktop/src/main/lib/custom-ringtones.ts
setCustomRingtoneDisplayName()関数を追加。表示名を80文字以下にトリムして永続化。
Frontend - YouTube Import Dialog
apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/RingtonesSettings/components/YouTubeImportDialog/YouTubeImportDialog.tsx, YouTubeImportDialog/index.ts
モーダルダイアログコンポーネント。URL、開始分秒、期間(1~30秒)、オプション表示名を入力。フォーム検証、送信状態管理、エラー表示機能を実装。
Frontend - Settings Integration
apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/RingtonesSettings/RingtonesSettings.tsx
「From YouTube」ボタンを追加。YouTube import dialog のオープン状態、エラー状態を管理。ミューテーション成功時にキャッシュを無効化、カスタム着信音に切り替え。エラーをダイアログに表示。

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI<br/>(RingtonesSettings)
    participant Dialog as YouTubeImportDialog
    participant TRPC as tRPC Router
    participant YouTube as YouTube<br/>Service
    participant FS as File<br/>System
    
    UI->>Dialog: Open dialog
    User->>Dialog: Enter URL, time range, name
    Dialog->>Dialog: Validate input
    User->>Dialog: Click Import
    Dialog->>UI: Call onSubmit()
    UI->>TRPC: importFromYouTube(url, startSeconds,<br/>durationSeconds, displayName?)
    TRPC->>YouTube: importRingtoneFromYouTube(options)
    YouTube->>YouTube: Validate URL & binaries
    YouTube->>FS: Download audio segment<br/>(yt-dlp --download-sections)
    FS-->>YouTube: Audio file
    YouTube->>FS: Convert to MP3 (ffmpeg)
    FS-->>YouTube: Converted MP3
    YouTube->>FS: Import via custom ringtone
    FS-->>YouTube: CustomRingtoneInfo
    YouTube->>FS: Set display name (optional)
    FS-->>YouTube: Updated metadata
    YouTube-->>TRPC: Return ringtone info
    TRPC-->>UI: Success response
    UI->>Dialog: Close dialog
    UI->>UI: Invalidate cache,<br/>switch to CUSTOM_RINGTONE_ID
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

複数ファイルにまたがる機能追加で、バックエンドのYouTubeダウンロード・変換ロジック(エラーハンドリング、外部プロセス実行、ファイル管理を含む)と、フロントエンドのフォーム検証・状態管理がそれぞれ独立した推論が必要。一部複雑なロジック密度があるが、コヒーシブなパターンで構成されている。

Poem

🐰 ぴょんぴょん、YouTubeから音を切り取り
フロントとバックで協力して
着信音をカスタマイズ、えっほい!
yt-dlpとffmpegよ、ありがとう
🎵 新しい通知音、誕生だ!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed タイトルは YouTube URL から通知音を作成するという主要な変更を正確に要約しており、PR の目的を明確に伝えています。
Description check ✅ Passed PR の説明は概要、実装内容、動作要件、テスト計画を含み、テンプレートの主要項目を網羅しており、十分に詳細です。
Linked Issues check ✅ Passed PR の実装内容は Issue #258 で要望されたユーザーが YouTube URL を貼って指定区間を切り取って通知音を作成する機能を完全に満たしています。
Out of Scope Changes check ✅ Passed すべての変更は YouTube から通知音を作成するという Issue #258 の範囲内であり、スコープ外の変更は見当たりません。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/258-youtube-ringtone

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/RingtonesSettings/RingtonesSettings.tsx (1)

380-384: .catch(() => {}) でエラーを握り潰す実装は不要で、ハンドラが同期的完了扱いになります。

onErroryoutubeError ステートに反映する設計なので、mutateAsync.catch は不要です。さらに現状の .catch(() => {})await の結果を正常完了にすり替えるため、onSubmitawait している呼び出し側(将来の追加実装含め)で「成功した」ように見えてしまう危険があります。mutate に切り替えるか、.catch を外して呼び出し元に伝播させる方が安全です。

♻️ 修正案
-				onSubmit={async (input) => {
-					await importFromYouTube.mutateAsync(input).catch(() => {
-						// Error surfaced via youtubeError state.
-					});
-				}}
+				onSubmit={(input) => {
+					importFromYouTube.mutate(input);
+				}}

これに合わせて YouTubeImportDialogonSubmit の型も Promise<void> から void に変更するか、mutateAsync のまま .catch を外すかを検討してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/RingtonesSettings/RingtonesSettings.tsx`
around lines 380 - 384, The onSubmit handler currently swallows errors by
awaiting importFromYouTube.mutateAsync(...).catch(() => {}); remove that .catch
so errors propagate (or switch to importFromYouTube.mutate to keep it
synchronous), and update the YouTubeImportDialog onSubmit type to void if you
choose mutate, or keep Promise<void> if you leave mutateAsync without .catch;
ensure youtubeError is still set via the existing onError handler on the
mutation (refer to importFromYouTube.mutateAsync, onSubmit, YouTubeImportDialog,
and the youtubeError state).
apps/desktop/src/main/lib/youtube-ringtone.ts (1)

181-189: displayName 指定時にメタデータが二度書き込まれます。

importCustomRingtoneFromPath(outputPath) の内部で、ファイル名 clip.mp3 を元にした sanitizeDisplayName により "clip" という表示名でメタデータが一旦書き込まれ、その直後 setCustomRingtoneDisplayName(displayName) で上書きされます。機能上は問題ありませんが、ディスク書き込みが 1 回余計に発生し、呼び出し経路も追いにくくなります。importCustomRingtoneFromPath に任意の displayName を渡せるオーバーロードを設けて一度で確定させる方がクリーンです(任意の refactor)。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/youtube-ringtone.ts` around lines 181 - 189, The
code currently calls importCustomRingtoneFromPath(outputPath) which writes
metadata using sanitizeDisplayName (e.g. "clip") and then immediately calls
setCustomRingtoneDisplayName(displayName) to overwrite it, causing an extra disk
write; change importCustomRingtoneFromPath to accept an optional displayName
parameter (or add an overload) and apply the same trim/slice/sanitize rules
there so the upstream call passes the user-specified displayName
(options.displayName?.trim()) and you can remove the follow-up
setCustomRingtoneDisplayName call; keep the existing sanitizeDisplayName,
trimming and 80-char slice behavior inside importCustomRingtoneFromPath to
preserve semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/lib/custom-ringtones.ts`:
- Around line 163-169: setCustomRingtoneDisplayName currently calls
writeCustomRingtoneMetadata which regenerates metadata and overwrites importedAt
with Date.now(); instead, change setCustomRingtoneDisplayName to read the
existing metadata via readCustomRingtoneMetadata(), update only the display name
(trim/limit to 80 or fallback to "Custom Audio"), preserve the existing
importedAt value, then write the merged metadata back using
writeCustomRingtoneMetadata (or a small helper that writes given metadata) so
importedAt is not changed; this prevents importCustomRingtoneFromPath /
importRingtoneFromYouTube flows or future name-only edits from accidentally
updating the import timestamp.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/RingtonesSettings/components/YouTubeImportDialog/YouTubeImportDialog.tsx`:
- Around line 67-75: The seconds field (startSec) isn't clamped to 0–59 before
converting to total seconds, so startSeconds (computed in YouTubeImportDialog)
can be wrong when users type values like 120; update the computation to clamp
startSec to the 0–59 range (e.g. apply a clamp/normalize function to
clampNonNegativeInt(startSec) with an upper limit of 59) before multiplying
startMin by 60 and adding seconds, and apply the same 0–59 clamp wherever
startSec is used (also the similar block around lines 134–142) to ensure correct
minute-second conversion regardless of browser UI constraints.

---

Nitpick comments:
In `@apps/desktop/src/main/lib/youtube-ringtone.ts`:
- Around line 181-189: The code currently calls
importCustomRingtoneFromPath(outputPath) which writes metadata using
sanitizeDisplayName (e.g. "clip") and then immediately calls
setCustomRingtoneDisplayName(displayName) to overwrite it, causing an extra disk
write; change importCustomRingtoneFromPath to accept an optional displayName
parameter (or add an overload) and apply the same trim/slice/sanitize rules
there so the upstream call passes the user-specified displayName
(options.displayName?.trim()) and you can remove the follow-up
setCustomRingtoneDisplayName call; keep the existing sanitizeDisplayName,
trimming and 80-char slice behavior inside importCustomRingtoneFromPath to
preserve semantics.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/RingtonesSettings/RingtonesSettings.tsx`:
- Around line 380-384: The onSubmit handler currently swallows errors by
awaiting importFromYouTube.mutateAsync(...).catch(() => {}); remove that .catch
so errors propagate (or switch to importFromYouTube.mutate to keep it
synchronous), and update the YouTubeImportDialog onSubmit type to void if you
choose mutate, or keep Promise<void> if you leave mutateAsync without .catch;
ensure youtubeError is still set via the existing onError handler on the
mutation (refer to importFromYouTube.mutateAsync, onSubmit, YouTubeImportDialog,
and the youtubeError state).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 20be24be-eef4-458d-8687-906c1aea6687

📥 Commits

Reviewing files that changed from the base of the PR and between 81c851e and 088b2fa.

📒 Files selected for processing (6)
  • apps/desktop/src/lib/trpc/routers/ringtone/index.ts
  • apps/desktop/src/main/lib/custom-ringtones.ts
  • apps/desktop/src/main/lib/youtube-ringtone.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/RingtonesSettings/RingtonesSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/RingtonesSettings/components/YouTubeImportDialog/YouTubeImportDialog.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/RingtonesSettings/components/YouTubeImportDialog/index.ts

Comment thread apps/desktop/src/main/lib/custom-ringtones.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 088b2fa96c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/main/lib/youtube-ringtone.ts Outdated
@MocA-Love
Copy link
Copy Markdown
Owner Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 088b2fa96c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/main/lib/youtube-ringtone.ts Outdated
Comment thread apps/desktop/src/main/lib/youtube-ringtone.ts Outdated
- yt-dlp/ffmpeg をログインシェルの PATH 経由で解決し、
  Finder/Dock 起動時の Homebrew パス欠落を回避
- ffprobe も事前チェックして欠落時に明示エラー
- yt-dlp の出力をテンプレート (.%(ext)s) で受け取り、
  実際に生成されたファイルをディレクトリスキャンで取得
- setCustomRingtoneDisplayName 呼び出しで importedAt を
  上書きしないように既存値を保持
- Start time の秒入力を 0-59 にクランプ
@MocA-Love MocA-Love merged commit b5b84d2 into main Apr 17, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SettingsのPERSONALのNitificationについて

1 participant