miniradio_server 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,312 @@
1
+ /* One audio element; streams and artwork are loaded only when selected. */
2
+ (() => {
3
+ const tracks = JSON.parse(document.getElementById('tracks').textContent);
4
+ const player = document.getElementById('player');
5
+ const tableBody = document.querySelector('#playlistTable tbody');
6
+ const ids = ['currentTrack', 'currentArtist', 'currentAlbum', 'currentArtwork', 'artworkPlaceholder',
7
+ 'playbackState', 'playerStatus', 'elapsedTime', 'durationTime', 'seekControl', 'volumeControl',
8
+ 'volumeControls', 'volumeHint', 'muteButton', 'customControls', 'startButton', 'playPauseButton',
9
+ 'prevButton', 'nextButton', 'loopButton', 'shuffleButton'];
10
+ const ui = Object.fromEntries(ids.map(id => [id, document.getElementById(id)]));
11
+ const labels = { idle: 'No track selected', loading: 'Loading', playing: 'Playing', paused: 'Paused',
12
+ buffering: 'Buffering', blocked: 'Press Play', error: 'Playback error', finished: 'Finished', unsupported: 'Unsupported browser' };
13
+ const rowStates = [];
14
+ let currentIndex = -1;
15
+ let isLooping = false;
16
+ let isShuffling = false;
17
+ let hls;
18
+ let selection = 0;
19
+ let session;
20
+ let state = 'idle';
21
+ let seekSelection;
22
+ let removeMediaListeners = () => {};
23
+
24
+ // Retain the hls.js / ManagedMediaSource policy used by this application.
25
+ player.disableRemotePlayback = true;
26
+
27
+ function formatTime(seconds) {
28
+ if (!Number.isFinite(seconds) || seconds < 0) return '--:--';
29
+ const total = Math.floor(seconds);
30
+ const minutes = Math.floor(total / 60);
31
+ const remainder = String(total % 60).padStart(2, '0');
32
+ return minutes >= 60 ? `${Math.floor(minutes / 60)}:${String(minutes % 60).padStart(2, '0')}:${remainder}` : `${minutes}:${remainder}`;
33
+ }
34
+
35
+ function activeMedia() {
36
+ return session?.ready && session.id === selection && session.source === (player.currentSrc || player.src);
37
+ }
38
+
39
+ function renderProgress() {
40
+ const ready = activeMedia() && !['error', 'unsupported', 'loading'].includes(state);
41
+ const duration = ready ? player.duration : NaN;
42
+ const valid = Number.isFinite(duration) && duration > 0;
43
+ ui.durationTime.textContent = formatTime(valid ? duration : NaN);
44
+ ui.seekControl.disabled = !valid || player.seekable.length === 0;
45
+ ui.seekControl.max = valid ? duration : 0;
46
+ if (seekSelection !== selection) {
47
+ const elapsed = ready && Number.isFinite(player.currentTime) ? player.currentTime : 0;
48
+ ui.elapsedTime.textContent = formatTime(elapsed);
49
+ ui.seekControl.value = valid ? Math.min(duration, Math.max(0, elapsed)) : 0;
50
+ }
51
+ }
52
+
53
+ function renderPlaybackState() {
54
+ const selected = currentIndex >= 0;
55
+ ui.playbackState.textContent = labels[state];
56
+ ui.playPauseButton.textContent = state === 'error' ? 'Retry' : ['playing', 'buffering'].includes(state) ? 'Pause' : 'Play';
57
+ ui.playPauseButton.disabled = !selected || ['loading', 'unsupported'].includes(state);
58
+ ui.prevButton.disabled = !selected;
59
+ ui.nextButton.disabled = !selected || (!(isShuffling && tracks.length > 1) && currentIndex === tracks.length - 1);
60
+ ui.startButton.disabled = ui.loopButton.disabled = ui.shuffleButton.disabled = tracks.length === 0;
61
+ [...tableBody.children].forEach((row, i) => {
62
+ const active = i === currentIndex;
63
+ row.classList.toggle('active', active);
64
+ if (active) row.setAttribute('aria-current', 'true');
65
+ else row.removeAttribute('aria-current');
66
+ rowStates[i].textContent = active ? labels[state] : '';
67
+ });
68
+ renderProgress();
69
+ }
70
+
71
+ function setState(next, message = '') {
72
+ state = next;
73
+ ui.playerStatus.textContent = message;
74
+ renderPlaybackState();
75
+ }
76
+
77
+ function streamError() {
78
+ if (session) {
79
+ session.ready = false;
80
+ session.wantsPlay = false;
81
+ session.attempt += 1;
82
+ }
83
+ player.pause();
84
+ setState('error', 'Unable to load this stream. Press Retry or select another track.');
85
+ }
86
+
87
+ function requestPlay() {
88
+ if (!activeMedia()) return;
89
+ const current = session;
90
+ const attempt = ++current.attempt;
91
+ current.wantsPlay = true;
92
+ player.play().catch(error => {
93
+ if (current !== session || attempt !== current.attempt || !current.wantsPlay) return;
94
+ current.wantsPlay = false;
95
+ if (error.name === 'NotSupportedError') streamError();
96
+ else setState('blocked', 'Playback could not start. Press Play to try again.');
97
+ });
98
+ }
99
+
100
+ function renderCurrentTrack(track, selectionId) {
101
+ ui.currentTrack.textContent = track.title || track.file;
102
+ ui.currentArtist.textContent = track.artist || 'Artist information unavailable';
103
+ ui.currentAlbum.textContent = track.album || 'Album information unavailable';
104
+ ui.currentArtwork.hidden = true;
105
+ ui.currentArtwork.removeAttribute('src');
106
+ ui.artworkPlaceholder.hidden = false;
107
+ if (!track.artwork_url) return;
108
+ // Separate requests keep stale load/error events away from the visible image.
109
+ const image = new Image();
110
+ image.addEventListener('load', () => {
111
+ if (selectionId !== selection) return;
112
+ ui.currentArtwork.src = image.src;
113
+ ui.currentArtwork.hidden = false;
114
+ ui.artworkPlaceholder.hidden = true;
115
+ });
116
+ image.addEventListener('error', () => {
117
+ if (selectionId !== selection) return;
118
+ ui.currentArtwork.hidden = true;
119
+ ui.artworkPlaceholder.hidden = false;
120
+ });
121
+ image.src = track.artwork_url;
122
+ }
123
+
124
+ function bindMedia(current) {
125
+ const listeners = {
126
+ playing: () => {
127
+ if (player.paused || player.ended || !current.wantsPlay) return;
128
+ setState('playing');
129
+ },
130
+ pause: () => {
131
+ if (!player.paused || player.ended || ['error', 'blocked', 'finished'].includes(state)) return;
132
+ current.wantsPlay = false;
133
+ current.attempt += 1;
134
+ setState('paused');
135
+ },
136
+ waiting: () => {
137
+ if (!player.paused && current.wantsPlay) setState('buffering', 'Buffering audio…');
138
+ },
139
+ ended: () => {
140
+ if (!player.ended || state === 'finished') return;
141
+ current.wantsPlay = false;
142
+ if (isLooping) playTrack(currentIndex);
143
+ else if ((isShuffling && tracks.length > 1) || currentIndex < tracks.length - 1) nextTrack();
144
+ else setState('finished', 'Playlist finished. Press Play all to restart.');
145
+ },
146
+ error: () => { if (player.error) streamError(); },
147
+ loadedmetadata: renderProgress,
148
+ durationchange: renderProgress,
149
+ progress: renderProgress,
150
+ timeupdate: renderProgress
151
+ };
152
+ const bound = Object.entries(listeners).map(([event, listener]) => {
153
+ const guarded = () => {
154
+ if (current === session && activeMedia()) listener();
155
+ };
156
+ player.addEventListener(event, guarded);
157
+ return [event, guarded];
158
+ });
159
+ removeMediaListeners = () => bound.forEach(([event, listener]) => player.removeEventListener(event, listener));
160
+ }
161
+
162
+ function playTrack(index) {
163
+ const track = tracks[index];
164
+ if (!track) return;
165
+ selection += 1;
166
+ removeMediaListeners();
167
+ session = undefined;
168
+ player.pause();
169
+ if (hls) hls.destroy();
170
+ hls = undefined;
171
+ player.removeAttribute('src');
172
+ player.load();
173
+ currentIndex = index;
174
+ seekSelection = undefined;
175
+ renderCurrentTrack(track, selection);
176
+ const current = { id: selection, ready: false, source: undefined, wantsPlay: true, attempt: 0 };
177
+ session = current;
178
+ setState('loading', 'Loading track…');
179
+ bindMedia(current);
180
+ if (typeof Hls === 'undefined' || !Hls.isSupported()) {
181
+ setState('unsupported', 'This browser cannot play HLS streams. Update your browser or reload the page to retry.');
182
+ return;
183
+ }
184
+ hls = new Hls();
185
+ hls.on(Hls.Events.MANIFEST_PARSED, () => {
186
+ if (current !== session || state === 'error') return;
187
+ current.ready = true;
188
+ current.source = player.currentSrc || player.src;
189
+ renderProgress();
190
+ requestPlay();
191
+ });
192
+ hls.on(Hls.Events.ERROR, (_event, data) => {
193
+ if (current === session && data.fatal) streamError();
194
+ });
195
+ hls.loadSource(track.url);
196
+ hls.attachMedia(player);
197
+ }
198
+
199
+ function nextTrack() {
200
+ if (currentIndex < 0) return;
201
+ if (isShuffling && tracks.length > 1) {
202
+ const offset = 1 + Math.floor(Math.random() * (tracks.length - 1));
203
+ playTrack((currentIndex + offset) % tracks.length);
204
+ } else if (currentIndex + 1 < tracks.length) playTrack(currentIndex + 1);
205
+ }
206
+
207
+ ui.startButton.addEventListener('click', () => playTrack(0));
208
+ ui.playPauseButton.addEventListener('click', () => {
209
+ if (ui.playPauseButton.disabled) return;
210
+ if (['error', 'finished'].includes(state)) playTrack(currentIndex);
211
+ else if (player.paused) requestPlay();
212
+ else {
213
+ session.wantsPlay = false;
214
+ session.attempt += 1;
215
+ player.pause();
216
+ }
217
+ });
218
+ ui.nextButton.addEventListener('click', nextTrack);
219
+ ui.prevButton.addEventListener('click', () => {
220
+ if (currentIndex >= 0) playTrack((currentIndex + tracks.length - 1) % tracks.length);
221
+ });
222
+ ui.loopButton.addEventListener('click', () => {
223
+ isLooping = !isLooping;
224
+ ui.loopButton.textContent = `Repeat track: ${isLooping ? 'ON' : 'OFF'}`;
225
+ ui.loopButton.setAttribute('aria-pressed', String(isLooping));
226
+ });
227
+ ui.shuffleButton.addEventListener('click', () => {
228
+ isShuffling = !isShuffling;
229
+ ui.shuffleButton.textContent = `Shuffle: ${isShuffling ? 'ON' : 'OFF'}`;
230
+ ui.shuffleButton.setAttribute('aria-pressed', String(isShuffling));
231
+ renderPlaybackState();
232
+ });
233
+
234
+ ui.seekControl.addEventListener('input', () => {
235
+ if (ui.seekControl.disabled) return;
236
+ seekSelection = selection;
237
+ ui.elapsedTime.textContent = formatTime(Number(ui.seekControl.value));
238
+ });
239
+ ui.seekControl.addEventListener('change', () => {
240
+ if (seekSelection !== selection || ui.seekControl.disabled) {
241
+ seekSelection = undefined;
242
+ renderProgress();
243
+ return;
244
+ }
245
+ const desired = Number(ui.seekControl.value);
246
+ const ranges = player.seekable;
247
+ let nearest;
248
+ for (let i = 0; i < ranges.length; i += 1) {
249
+ const candidate = Math.max(ranges.start(i), Math.min(ranges.end(i), desired));
250
+ if (nearest === undefined || Math.abs(candidate - desired) < Math.abs(nearest - desired)) nearest = candidate;
251
+ }
252
+ seekSelection = undefined;
253
+ if (Number.isFinite(nearest)) player.currentTime = nearest;
254
+ renderProgress();
255
+ });
256
+ ui.seekControl.addEventListener('blur', () => {
257
+ seekSelection = undefined;
258
+ renderProgress();
259
+ });
260
+
261
+ function renderVolume() {
262
+ ui.volumeControl.value = player.volume;
263
+ ui.muteButton.textContent = player.muted ? 'Unmute' : 'Mute';
264
+ ui.muteButton.setAttribute('aria-pressed', String(player.muted));
265
+ }
266
+ // Some devices expose volume but do not allow scripts to change it.
267
+ const originalVolume = player.volume;
268
+ let canChangeVolume = false;
269
+ try {
270
+ const probe = originalVolume > 0.5 ? 0.25 : 0.75;
271
+ player.volume = probe;
272
+ canChangeVolume = player.volume === probe;
273
+ } catch (_) {
274
+ canChangeVolume = false;
275
+ } finally {
276
+ try { player.volume = originalVolume; } catch (_) { /* Device volume is read-only. */ }
277
+ }
278
+ ui.volumeControls.hidden = !canChangeVolume;
279
+ ui.volumeHint.hidden = canChangeVolume;
280
+ ui.volumeControl.addEventListener('input', () => { player.volume = Number(ui.volumeControl.value); });
281
+ ui.muteButton.addEventListener('click', () => { player.muted = !player.muted; });
282
+ player.addEventListener('volumechange', renderVolume);
283
+ renderVolume();
284
+
285
+ tracks.forEach((track, index) => {
286
+ const row = document.createElement('tr');
287
+ const playCell = document.createElement('td');
288
+ const button = document.createElement('button');
289
+ button.textContent = '▶';
290
+ button.setAttribute('aria-label', `Play ${track.title || track.file}`);
291
+ button.addEventListener('click', () => playTrack(index));
292
+ playCell.dataset.label = 'Play';
293
+ playCell.appendChild(button);
294
+ row.appendChild(playCell);
295
+ [[track.title || track.file, 'Title'], [track.artist || '', 'Artist'], [track.album || '', 'Album']].forEach(([text, label]) => {
296
+ const cell = document.createElement('td');
297
+ cell.textContent = text;
298
+ cell.dataset.label = label;
299
+ row.appendChild(cell);
300
+ });
301
+ const rowState = document.createElement('td');
302
+ rowState.dataset.label = 'Status';
303
+ rowState.classList.add('track-status');
304
+ rowStates.push(rowState);
305
+ row.appendChild(rowState);
306
+ tableBody.appendChild(row);
307
+ });
308
+ setState('idle', tracks.length ? '' : 'No MP3 files found.');
309
+ ui.customControls.hidden = false;
310
+ player.controls = false;
311
+ player.hidden = true;
312
+ })();
@@ -1,26 +1,60 @@
1
1
  @charset "utf-8";
2
2
 
3
- table.compact td, table.compact th {
4
- padding: 0.4rem 0.5rem;
5
- font-size: 0.9rem;
6
- line-height: 1.2;
3
+ [hidden] { display: none !important; }
4
+ #player { width: 100%; }
5
+ .now-playing {
6
+ background: var(--pico-card-background-color);
7
+ border: 1px solid var(--pico-muted-border-color);
8
+ border-radius: var(--pico-border-radius);
9
+ padding: 1.25rem;
7
10
  }
11
+ .track-details { display: grid; grid-template-columns: 144px minmax(0, 1fr); gap: 1.5rem; align-items: center; }
12
+ .artwork { width: 144px; aspect-ratio: 1; background: var(--pico-background-color); border-radius: var(--pico-border-radius); overflow: hidden; }
13
+ .artwork img { width: 100%; height: 100%; object-fit: contain; }
14
+ #artworkPlaceholder { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--pico-muted-color); }
15
+ #artworkPlaceholder span { font-size: 2.5rem; }
16
+ #artworkPlaceholder small { font-size: 0.65rem; }
17
+ .track-metadata { min-width: 0; overflow-wrap: anywhere; }
18
+ .track-metadata h2 { margin: 0.4rem 0; }
19
+ .track-metadata p { margin: 0; }
20
+ #playbackState { color: var(--pico-primary); font-size: 0.8rem; }
21
+ #currentArtist, #currentAlbum { color: var(--pico-muted-color); }
22
+ #currentAlbum { font-size: 0.85rem; }
23
+ .time-labels { display: flex; justify-content: space-between; margin-top: 1rem; color: var(--pico-muted-color); font-size: 0.8rem; font-variant-numeric: tabular-nums; }
24
+ #seekControl { width: 100%; margin: 0; min-height: 44px; }
25
+ .transport, .playback-options, .volume { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
26
+ .transport button, .playback-options button, .volume button { width: auto; min-height: 44px; margin: 0; padding: 0.5rem 0.75rem; }
27
+ .transport button:not(#playPauseButton), .playback-options button { --pico-background-color: transparent; --pico-color: var(--pico-primary); }
28
+ #playPauseButton { min-width: 7rem; }
29
+ .volume { margin-left: auto; font-size: 0.8rem; }
30
+ .volume label { margin: 0; }
31
+ #volumeControl { width: 90px; margin: 0; min-height: 44px; }
32
+ .playback-options { border-top: 1px solid var(--pico-muted-border-color); padding-top: 1rem; margin-top: 1rem; font-size: 0.8rem; }
33
+ .playback-options button[aria-pressed="true"] { background: var(--pico-primary); color: var(--pico-primary-inverse); }
34
+ #playerStatus { margin: 1rem 0 0; font-size: 0.85rem; overflow-wrap: anywhere; }
35
+ #playerStatus:empty { margin: 0; }
36
+ .playlist-heading { margin-top: 1.5rem; }
37
+ tr.active { outline: 2px solid var(--pico-primary); outline-offset: -2px; }
38
+ .track-status { color: var(--pico-primary); }
39
+ table.compact td, table.compact th { padding: 0.4rem 0.5rem; font-size: 0.9rem; line-height: 1.2; overflow-wrap: anywhere; }
40
+ table.compact { table-layout: fixed; }
41
+ table.compact th:first-child { width: 4rem; }
42
+ table.compact button { margin: 0; min-width: 44px; min-height: 44px; padding: 0.4rem; }
8
43
  @media screen and (max-width: 600px) {
9
- table thead {
10
- display: none;
11
- }
12
- table tr {
13
- display: block;
14
- margin-bottom: 0.8rem;
15
- }
16
- table td {
17
- display: flex;
18
- justify-content: space-between;
19
- padding: 0.3rem 0;
20
- border-bottom: 1px solid #ccc;
21
- }
22
- table td::before {
23
- content: attr(data-label);
24
- font-weight: bold;
25
- }
44
+ .now-playing { padding: 0.8rem; }
45
+ .track-details { grid-template-columns: 80px minmax(0, 1fr); gap: 0.8rem; }
46
+ .artwork { width: 80px; }
47
+ #artworkPlaceholder small { display: none; }
48
+ .track-metadata h2 { font-size: 1.2rem; }
49
+ .transport { gap: 0.4rem; }
50
+ .transport > button { flex: 1; padding: 0.4rem; font-size: 0.8rem; }
51
+ #playPauseButton { min-width: 0; }
52
+ .volume { margin-left: 0; width: 100%; }
53
+ #volumeControl { flex: 1; min-width: 0; }
54
+ table thead { display: none; }
55
+ table tr { display: grid; grid-template-columns: 44px minmax(0, 1fr); column-gap: 0.75rem; margin-bottom: 0.8rem; padding: 0.6rem; }
56
+ table td { display: block; grid-column: 2; padding: 0.15rem 0; border: none; }
57
+ table td:first-child { grid-column: 1; grid-row: 1 / span 5; align-self: center; }
58
+ table td[data-label="Artist"], table td[data-label="Album"] { color: var(--pico-muted-color); font-size: 0.8rem; }
59
+ table td:empty { display: none; }
26
60
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: miniradio_server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Koichiro Ohba
@@ -52,19 +52,81 @@ dependencies:
52
52
  - !ruby/object:Gem::Version
53
53
  version: '5.16'
54
54
  - !ruby/object:Gem::Dependency
55
- name: rackup
55
+ name: standard
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: simplecov
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.3'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.3'
82
+ - !ruby/object:Gem::Dependency
83
+ name: bundler-audit
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.9'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.9'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rack
56
98
  requirement: !ruby/object:Gem::Requirement
57
99
  requirements:
58
100
  - - ">="
59
101
  - !ruby/object:Gem::Version
60
- version: '0'
102
+ version: '3.1'
103
+ - - "<"
104
+ - !ruby/object:Gem::Version
105
+ version: '4'
61
106
  type: :runtime
62
107
  prerelease: false
63
108
  version_requirements: !ruby/object:Gem::Requirement
64
109
  requirements:
65
110
  - - ">="
66
111
  - !ruby/object:Gem::Version
67
- version: '0'
112
+ version: '3.1'
113
+ - - "<"
114
+ - !ruby/object:Gem::Version
115
+ version: '4'
116
+ - !ruby/object:Gem::Dependency
117
+ name: rackup
118
+ requirement: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '2.2'
123
+ type: :runtime
124
+ prerelease: false
125
+ version_requirements: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '2.2'
68
130
  - !ruby/object:Gem::Dependency
69
131
  name: webrick
70
132
  requirement: !ruby/object:Gem::Requirement
@@ -154,27 +216,30 @@ description: |2
154
216
  This server is designed for simplicity and primarily targets Video on Demand (VOD) scenarios where you want to stream existing MP3 files via HLS without pre-converting them.
155
217
  email:
156
218
  - koichiro.ohba@gmail.com
157
- executables: []
219
+ executables:
220
+ - miniradio_server
158
221
  extensions: []
159
222
  extra_rdoc_files: []
160
223
  files:
224
+ - CHANGELOG.md
161
225
  - LICENSE.txt
162
226
  - README.md
163
- - Rakefile
164
- - hls_cache/.gitkeep
227
+ - exe/miniradio_server
165
228
  - lib/miniradio_server.rb
166
229
  - lib/miniradio_server/app.rb
230
+ - lib/miniradio_server/cli.rb
167
231
  - lib/miniradio_server/templ/index.html.slim
168
232
  - lib/miniradio_server/version.rb
233
+ - lib/public/player.js
169
234
  - lib/public/style/main.css
170
- - mp3_files/.gitkeep
171
- - sig/miniradio_server.rbs
172
235
  homepage: https://github.com/koichiro/miniradio_server
173
236
  licenses:
174
237
  - MIT
175
238
  metadata:
239
+ allowed_push_host: https://rubygems.org
176
240
  homepage_uri: https://github.com/koichiro/miniradio_server
177
241
  source_code_uri: https://github.com/koichiro/miniradio_server.git
242
+ changelog_uri: https://github.com/koichiro/miniradio_server/blob/main/CHANGELOG.md
178
243
  rdoc_options: []
179
244
  require_paths:
180
245
  - lib
@@ -182,14 +247,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
182
247
  requirements:
183
248
  - - ">="
184
249
  - !ruby/object:Gem::Version
185
- version: 3.1.0
250
+ version: 3.4.0
186
251
  required_rubygems_version: !ruby/object:Gem::Requirement
187
252
  requirements:
188
253
  - - ">="
189
254
  - !ruby/object:Gem::Version
190
255
  version: '0'
191
256
  requirements: []
192
- rubygems_version: 3.6.7
257
+ rubygems_version: 4.0.20
193
258
  specification_version: 4
194
259
  summary: Miniradio Server is Simple Ruby HLS Server for MP3s.
195
260
  test_files: []
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "minitest/test_task"
5
-
6
- Minitest::TestTask.create
7
-
8
- task default: :test
data/hls_cache/.gitkeep DELETED
File without changes
data/mp3_files/.gitkeep DELETED
File without changes
@@ -1,4 +0,0 @@
1
- module MiniradioServer
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end