httmpc 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/Gemfile +4 -0
- data/LICENSE +8 -0
- data/README.md +102 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/httmpc +200 -0
- data/httmpc.gemspec +34 -0
- data/httmpc.service +14 -0
- data/lib/httmpc.rb +2 -0
- data/lib/httmpc/song.rb +58 -0
- data/lib/httmpc/version.rb +3 -0
- data/public/css/bootstrap-theme.css +587 -0
- data/public/css/bootstrap-theme.css.map +1 -0
- data/public/css/bootstrap-theme.min.css +6 -0
- data/public/css/bootstrap-theme.min.css.map +1 -0
- data/public/css/bootstrap.css +6757 -0
- data/public/css/bootstrap.css.map +1 -0
- data/public/css/bootstrap.min.css +6 -0
- data/public/css/bootstrap.min.css.map +1 -0
- data/public/css/bootstrap_custom.css +5 -0
- data/public/css/notyf.min.css +1 -0
- data/public/css/theme.bootstrap_3.min.css +1 -0
- data/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/public/fonts/glyphicons-halflings-regular.svg +288 -0
- data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/public/fonts/glyphicons-halflings-regular.woff2 +0 -0
- data/public/js/bootstrap.js +2377 -0
- data/public/js/bootstrap.min.js +7 -0
- data/public/js/jquery.tablesorter.min.js +1 -0
- data/public/js/jquery.tablesorter.widgets.min.js +2 -0
- data/public/js/notyf.min.js +1 -0
- data/public/js/npm.js +13 -0
- data/views/listall.erb +87 -0
- data/views/playlist.erb +76 -0
- data/views/template.erb +210 -0
- metadata +181 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: dd8a6a07c3f8561bcb3ba49a8099d65211b76ccd1427b0305b5bf03811e13a5f
|
4
|
+
data.tar.gz: d381ad4686df3d5bfae6366279d6dfe68cd8a569199fec3cdf4d1d9981c820b8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: aa9ae1eb4398d06bb1c52304d74ef98198d54c3616fe6d7696e73eb3df17d3cdd4cb1553f50d62951b56b8ad8f6125cd8db8c6eece2bca8c071e15e2adbbde15
|
7
|
+
data.tar.gz: 0b5096064ff6f5d0303febc34580c4590354e1b45bfb48963c0035b16e600340218d212a1f28a74538cfec1874f97283a99e5d6dd15b2d38c26f16ea7b5a83d2
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
Copyright 2020 ASAHI,Michiharu
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
8
|
+
|
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# httmpc
|
2
|
+
|
3
|
+
mpd サーバのフロントエンドとなる web サーバです。
|
4
|
+
httmpc が動作するサーバにブラウザでアクセスすることで、mpd サーバの選曲や再生ができます。
|
5
|
+
|
6
|
+
主な使い方として、Raspberry Pi 上に [rbtune](https://github.com/fusuian/rbtune) ともどもインストールして、自動録音されたラジオ番組を再生すること(また、聞き終わった番組を手軽に消去すること)を想定しています。
|
7
|
+
mp3 などのアルバムのコレクションからの再生には向かないので、その場合は他の mpd クライアントをご利用ください。
|
8
|
+
|
9
|
+
|
10
|
+
## インストールと設定
|
11
|
+
|
12
|
+
あらかじめ mpd をインストールする必要があります。
|
13
|
+
|
14
|
+
$ sudo apt install mpd
|
15
|
+
|
16
|
+
httmpc 本体は gem でインストールできます。Ruby 2.5以降が必要です。
|
17
|
+
|
18
|
+
$ gem install httmpc
|
19
|
+
|
20
|
+
|
21
|
+
## 使い方
|
22
|
+
|
23
|
+
### サーバの起動
|
24
|
+
以下のコマンドで httmpc が起動します。
|
25
|
+
|
26
|
+
$ httmpc -e production
|
27
|
+
|
28
|
+
コマンドラインオプションはsinatraに準じます、
|
29
|
+
|
30
|
+
Usage: httmpc [options]
|
31
|
+
-p port set the port (default is 4567)
|
32
|
+
-s server specify rack server/handler (default is thin)
|
33
|
+
-q turn on quiet mode (default is off)
|
34
|
+
-x turn on the mutex lock (default is off)
|
35
|
+
-e env set the environment (default is development)
|
36
|
+
-o addr set the host (default is (env == 'development' ? 'localhost' : '0.0.0.0'))
|
37
|
+
|
38
|
+
ただし、ポートを指定する-pオプションは効果がないので、環境変数PORTで設定します。
|
39
|
+
|
40
|
+
$ PORT=80 httmpc -e production
|
41
|
+
|
42
|
+
|
43
|
+
### ブラウザからのアクセス
|
44
|
+
|
45
|
+
ホスト recorder.local 上でhttmpcを起動した場合、ブラウザから以下のアドレスにアクセスすると、音声ファイルのリストが表示されます。
|
46
|
+
|
47
|
+
デフォルトで起動した場合:
|
48
|
+
http://recorder.local:4567/
|
49
|
+
|
50
|
+
PORT=80 で起動した場合:
|
51
|
+
http://recorder.local/
|
52
|
+
|
53
|
+
|
54
|
+
#### All List タブ
|
55
|
+
|
56
|
+
All List タブをクリックすると、録音された音声ファイルの一覧が表示されます。
|
57
|
+
タイトル、日付、時:分 でソートできます。音声ファイル名がボタンになっているので、これをクリックするとPlaylist に追加されます。右端のゴミ箱アイコンをクリックするとファイルを削除します。
|
58
|
+
録音されたはずのファイルが表示されない場合は、ブラウザからリロードを繰り返すと表示されます。
|
59
|
+
|
60
|
+
|
61
|
+
#### Playlist タブ
|
62
|
+
Playlist タブをクリックすると、プレイリストが表示されます。
|
63
|
+
これもタイトル、日付、時:分 でソートでき、音声ファイル名のボタンを押すと再生が始まります。
|
64
|
+
再生が済んでもプレイリストから自動的に消えることはないので、右端のxボタンを押して音声ファイルをプレイリストから取り除きます。(ファイルは消えません)
|
65
|
+
|
66
|
+
|
67
|
+
#### コントロールパネル
|
68
|
+
どちらのタブでも、上部にはコントロールパネルがあります。
|
69
|
+
コントロールパネルの機能は以下のようになっています。(上から)
|
70
|
+
* 再生中のタイトルとプレイリストの進む・戻るボタン
|
71
|
+
* 再生位置バー
|
72
|
+
* 30秒戻る、15秒戻る、再生開始、一時停止、15秒進む、30秒進む
|
73
|
+
* 15分戻る、5分戻る、1分戻る、1分進む、5分進む、15分進む
|
74
|
+
|
75
|
+
|
76
|
+
### サービスとして登録する
|
77
|
+
|
78
|
+
httmpcはWebサーバなので、コマンドラインから起動するよりも、自動起動されるようにサービスとして登録する方が便利です。
|
79
|
+
Raspberry piの場合、以下のような httmpc.service を用意して /etc/systemd/system/ に配置して、
|
80
|
+
|
81
|
+
$ sudo systemctl enable httmpc
|
82
|
+
|
83
|
+
としてから再起動すると、OSが起動するときに自動的に httmpc が起動するようになります。
|
84
|
+
|
85
|
+
[Unit]
|
86
|
+
Description=httmpc - run mpd Web frontend
|
87
|
+
After=network.target
|
88
|
+
|
89
|
+
[Service]
|
90
|
+
Type = simple
|
91
|
+
Environment=PORT=80
|
92
|
+
ExecStart = /usr/local/bin/httmpc -e production
|
93
|
+
Restart = always
|
94
|
+
|
95
|
+
[Install]
|
96
|
+
WantedBy=multi-user.target
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
## プロジェクトへの協力
|
101
|
+
|
102
|
+
バグ報告やプルリクエストは Github の https://github.com/fusuian/httmpc まで、よろしくお願いします。
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "httmpc"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/httmpc
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "sinatra"
|
3
|
+
require "sys-filesystem"
|
4
|
+
require "net/telnet"
|
5
|
+
require "time"
|
6
|
+
require "httmpc"
|
7
|
+
|
8
|
+
$mpdhost = "localhost"
|
9
|
+
$mpdport = 6600
|
10
|
+
$music_dir = '/home/pi/radio'
|
11
|
+
|
12
|
+
set :views, File.join(File.dirname(__FILE__), '../views')
|
13
|
+
set :public_folder, File.join(File.dirname(__FILE__), '../public')
|
14
|
+
#set :port, 80
|
15
|
+
|
16
|
+
get '/' do
|
17
|
+
redirect '/listall'
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
# ディスク残量(GB)
|
22
|
+
def available
|
23
|
+
stat = Sys::Filesystem.stat('/')
|
24
|
+
available = (stat.blocks_available * stat.block_size).to_f / 1024 / 1024 / 1024
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# ディスク容量(GB)
|
29
|
+
def total
|
30
|
+
stat = Sys::Filesystem.stat('/')
|
31
|
+
total = (stat.blocks * stat.block_size).to_f / 1024 / 1024 / 1024
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def fetch_songs(cmd)
|
36
|
+
songs = []
|
37
|
+
info = []
|
38
|
+
res = term {|t| t.cmd(cmd)}
|
39
|
+
return [] unless res # 曲がない
|
40
|
+
|
41
|
+
infos = res.split(/\n/)
|
42
|
+
infos.pop
|
43
|
+
infos.each do |s|
|
44
|
+
if s =~ /^file:/ && info.size > 0
|
45
|
+
songs << Song.new(info)
|
46
|
+
info = []
|
47
|
+
end
|
48
|
+
info << s
|
49
|
+
end
|
50
|
+
songs << Song.new(info) if info.size > 0
|
51
|
+
songs
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def term
|
56
|
+
term = Net::Telnet.new("Host" => $mpdhost, "Port" => $mpdport,
|
57
|
+
"Prompt"=>/OK/, "Telnetmode" => false, "Binmode" => true)
|
58
|
+
term.waitfor /^OK.*$/
|
59
|
+
res = yield term
|
60
|
+
term.close
|
61
|
+
res
|
62
|
+
end
|
63
|
+
|
64
|
+
post '/add/:file' do |file|
|
65
|
+
content_type :json
|
66
|
+
res = term {|t| t.cmd "add #{file}" }
|
67
|
+
res.force_encoding('utf-8') if res
|
68
|
+
{action: 'add', file: file, responce: res}.to_json
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
delete '/delete/:id' do |id|
|
73
|
+
content_type :json
|
74
|
+
res = term {|t| t.cmd "deleteid #{id}" }
|
75
|
+
{action: 'delete', id: id, responce: res}.to_json
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
post '/play/:id' do |id|
|
80
|
+
content_type :json
|
81
|
+
res = term {|t| t.cmd("playid #{id}") }
|
82
|
+
res.force_encoding('utf-8') if res
|
83
|
+
{action: 'play', id: id, responce: res}.to_json
|
84
|
+
end
|
85
|
+
|
86
|
+
post '/play' do
|
87
|
+
content_type :text
|
88
|
+
term {|t| t.cmd("play") }
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
post '/pause' do
|
93
|
+
content_type :text
|
94
|
+
term {|t| t.cmd("pause") }
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
post '/next' do
|
99
|
+
content_type :text
|
100
|
+
term {|t| t.cmd("next") }
|
101
|
+
end
|
102
|
+
|
103
|
+
post '/previous' do
|
104
|
+
content_type :text
|
105
|
+
term {|t| t.cmd("previous") }
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
post '/seekcur/:sec' do |sec|
|
110
|
+
content_type :text
|
111
|
+
res = term {|t| t.cmd("seekcur #{sec}") }
|
112
|
+
"#{res}: seekcur #{sec}"
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
get '/playlist' do
|
117
|
+
begin
|
118
|
+
@title = "PlayList"
|
119
|
+
@songs = fetch_songs 'playlistinfo'
|
120
|
+
erb :playlist, layout: :template
|
121
|
+
|
122
|
+
rescue Errno::ECONNREFUSED
|
123
|
+
"#{$mpdhost}:#{$mpdport} mpdホストへの接続失敗"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
get '/listall' do
|
129
|
+
begin
|
130
|
+
@title = "AllList"
|
131
|
+
res = term {|t| t.cmd("update") }
|
132
|
+
@songs = fetch_songs 'listallinfo'
|
133
|
+
@songs = @songs.sort_by {|s| s.onair }.reverse
|
134
|
+
|
135
|
+
erb :listall, layout: :template
|
136
|
+
|
137
|
+
rescue Errno::ECONNREFUSED
|
138
|
+
"#{$mpdhost}:#{$mpdport} mpdホストへの接続失敗"
|
139
|
+
|
140
|
+
rescue Encoding::CompatibilityError
|
141
|
+
"listallinfoのエンコード失敗"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
def stat2hash(res)
|
147
|
+
stats = res.split(/\n/)
|
148
|
+
stats.pop
|
149
|
+
hash = {}
|
150
|
+
stats.each do |s|
|
151
|
+
k,v = s.split(/: /)
|
152
|
+
hash[k] = v
|
153
|
+
end
|
154
|
+
hash
|
155
|
+
end
|
156
|
+
|
157
|
+
get '/playing' do
|
158
|
+
begin
|
159
|
+
res = term {|t| t.cmd('status') }
|
160
|
+
return {}.to_json unless res
|
161
|
+
status = stat2hash(res)
|
162
|
+
|
163
|
+
playing = {}
|
164
|
+
playing['state'] = status['state']
|
165
|
+
|
166
|
+
if playing['state'] == 'stop'
|
167
|
+
playing['title'] = 'stopping'
|
168
|
+
playing['elapsed'] = '--:--'
|
169
|
+
playing['time'] = '--:--'
|
170
|
+
|
171
|
+
else
|
172
|
+
playing['elapsed'] = status['elapsed'].to_f
|
173
|
+
songs = fetch_songs('currentsong')
|
174
|
+
song = songs[0]
|
175
|
+
playing['title'] = song.title
|
176
|
+
playing['onair'] = song.onair if song.has_key? 'onair'
|
177
|
+
playing['time'] = song.time.to_i
|
178
|
+
end
|
179
|
+
|
180
|
+
playing.to_json
|
181
|
+
|
182
|
+
rescue Errno::ECONNRESET, Errno::EPIPE
|
183
|
+
return {}.to_json
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
delete '/kill_file/:file' do |file|
|
190
|
+
content_type :json
|
191
|
+
begin
|
192
|
+
FileUtils.rm File.join($music_dir, file)
|
193
|
+
{action: 'delete', file: file, responce: 'done'}.to_json
|
194
|
+
rescue Errno::ENOENT
|
195
|
+
status 404
|
196
|
+
"#{file}が存在しません"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
run Sinatra::Application.run!
|
data/httmpc.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "httmpc/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "httmpc"
|
8
|
+
spec.version = Httmpc::VERSION
|
9
|
+
spec.authors = ["ASAHI,Michiharu"]
|
10
|
+
spec.email = ["fusuian@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{mpd client for browser}
|
13
|
+
spec.description = %q{An mpd client that acts as a web server. You can operate the mpd server from any browser.}
|
14
|
+
spec.homepage = "https://github.com/fusuian/httmpc"
|
15
|
+
spec.license = "MIT"
|
16
|
+
spec.required_ruby_version = Gem::Requirement.new("~> 2.5")
|
17
|
+
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
20
|
+
f.match(%r{^(test|spec|features)/})
|
21
|
+
end
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.executables = %w(httmpc)
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
|
26
|
+
spec.add_dependency "net-telnet", "~> 0.2"
|
27
|
+
spec.add_dependency "sys-filesystem", "~> 1.3"
|
28
|
+
spec.add_dependency "thin", "~> 1.7"
|
29
|
+
spec.add_dependency "sinatra", "~> 2.0"
|
30
|
+
spec.add_dependency "sinatra-contrib", "~> 2.0"
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler", "~> 2"
|
33
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
34
|
+
end
|
data/httmpc.service
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
[Unit]
|
2
|
+
Description=httmpc - run mpd Web frontend
|
3
|
+
After=network.target
|
4
|
+
|
5
|
+
[Service]
|
6
|
+
Type = simple
|
7
|
+
Environment=PORT=80
|
8
|
+
ExecStart = /usr/local/bin/httmpc -e production
|
9
|
+
#Restart = always
|
10
|
+
Restart = no
|
11
|
+
|
12
|
+
[Install]
|
13
|
+
WantedBy=multi-user.target
|
14
|
+
|
data/lib/httmpc.rb
ADDED
data/lib/httmpc/song.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require "date"
|
2
|
+
|
3
|
+
class Song < Hash
|
4
|
+
def initialize(args)
|
5
|
+
args.each do |s|
|
6
|
+
k,v = s.split(/: /)
|
7
|
+
k.downcase!
|
8
|
+
case k
|
9
|
+
when 'file'
|
10
|
+
Song.parse_file self, k, v
|
11
|
+
when /-modified/
|
12
|
+
self[k] = DateTime.parse v
|
13
|
+
self['onair'] = self[k] unless self.has_key? 'onair'
|
14
|
+
when 'time'
|
15
|
+
self[k] = v.to_f
|
16
|
+
when 'pos', 'id'
|
17
|
+
self[k] = v.to_i
|
18
|
+
when 'title'
|
19
|
+
else
|
20
|
+
v.force_encoding(Encoding::UTF_8) unless v.encoding == Encoding::UTF_8
|
21
|
+
self[k] = v
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def playtime
|
28
|
+
if self.has_key? 'time'
|
29
|
+
(Time.parse("1/1") + time).strftime("%H:%M")
|
30
|
+
else
|
31
|
+
"?"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.parse_file(hash,k,v)
|
36
|
+
v.force_encoding('utf-8')
|
37
|
+
hash[k] = v
|
38
|
+
v =~ /\.(\d{4}-\d{2}-\d{2}(T\d{2}=\d{2})?)\./
|
39
|
+
if $1
|
40
|
+
onair = $1.sub(/=/, ":")
|
41
|
+
hash['onair'] = DateTime.parse onair
|
42
|
+
hash['title'] = v.split(/\./)[0]
|
43
|
+
else
|
44
|
+
hash['title'] = v
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def method_missing(method_name)
|
50
|
+
method_key = method_name.to_s
|
51
|
+
if self.key? method_key
|
52
|
+
self[method_key]
|
53
|
+
else
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|