httmpc 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /vendor/
11
+ ._*
12
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in httmpc.gemspec
4
+ gemspec
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
+
@@ -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 まで、よろしくお願いします。
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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!
@@ -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
@@ -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
+
@@ -0,0 +1,2 @@
1
+ require "httmpc/version"
2
+ require "httmpc/song"
@@ -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