/lib/Teto/Track.pm

http://github.com/motemen/Teto · Perl · 455 lines · 364 code · 77 blank · 14 comment · 23 complexity · 8943c22b5a5680ecaa453c6a60e82cf3 MD5 · raw file

  1. package Teto::Track;
  2. use Mouse;
  3. use MouseX::Types::URI;
  4. use AnyEvent;
  5. use AnyEvent::Util;
  6. use AnyEvent::HTTP;
  7. use AnyEvent::Handle;
  8. use Coro;
  9. # use Coro::LWP; # load in teto.pl
  10. use Coro::AIO;
  11. use Coro::Timer ();
  12. use LWP::UserAgent;
  13. use HTTP::Request::Common;
  14. use Class::Load;
  15. use Path::Class;
  16. use File::Temp ();
  17. use Cache::LRU::Peekable;
  18. use Scalar::Util qw(refaddr weaken);
  19. use overload '""' => 'as_string', fallback => 1;
  20. with 'Teto::Role::Log';
  21. has url => (
  22. is => 'rw',
  23. isa => 'URI',
  24. required => 1,
  25. coerce => 1,
  26. );
  27. has media_url => (
  28. is => 'rw',
  29. isa => 'Maybe[Str]', # Maybe[URI]
  30. coerce => 1,
  31. lazy_build => 1,
  32. );
  33. has title => (
  34. is => 'rw',
  35. isa => 'Maybe[Str]',
  36. );
  37. has image => (
  38. is => 'rw',
  39. isa => 'Maybe[Str]',
  40. );
  41. has user_agent => (
  42. is => 'rw',
  43. isa => 'LWP::UserAgent',
  44. default => sub { LWP::UserAgent->new(show_progress => 1) }
  45. );
  46. has error => (
  47. is => 'rw',
  48. isa => 'Str',
  49. );
  50. has buffer_signal => (
  51. is => 'rw',
  52. isa => 'Coro::Signal',
  53. default => sub { Coro::Signal->new },
  54. );
  55. before error => sub {
  56. my $self = shift;
  57. if (@_) {
  58. $self->log(error => @_);
  59. $self->done;
  60. }
  61. };
  62. ### Track status
  63. use constant {
  64. TRACK_STATUS_STANDBY => 'standby',
  65. TRACK_STATUS_PLAYING => 'playing',
  66. TRACK_STATUS_DONE => 'done',
  67. };
  68. has status => (
  69. is => 'rw',
  70. isa => 'Str',
  71. default => 'standby',
  72. );
  73. sub is_standby {
  74. my $self = shift;
  75. return 1 if not $self->has_buffer;
  76. return 1 if $self->status eq TRACK_STATUS_STANDBY;
  77. return 0;
  78. }
  79. sub is_playing {
  80. my $self = shift;
  81. return 0 unless $self->status eq TRACK_STATUS_PLAYING;
  82. return 0 unless $self->has_buffer;
  83. return 1;
  84. }
  85. sub is_done {
  86. my $self = shift;
  87. return 0 unless $self->status eq TRACK_STATUS_DONE;
  88. return 0 unless $self->has_buffer;
  89. return 1;
  90. }
  91. sub done {
  92. my $self = shift;
  93. $self->status(TRACK_STATUS_DONE);
  94. # $self->log(debug => 'broadcast buffer_signal');
  95. $self->buffer_signal->broadcast;
  96. }
  97. __PACKAGE__->meta->make_immutable;
  98. no Mouse;
  99. ### Buffers
  100. # buffers are stored in Cache::LRU as reference, so recently
  101. # unused buffers are automatically purged.
  102. # TODO FIXME currently playing track's buffer should not be purged!!!
  103. our $BufferCache = Cache::LRU::Peekable->new(size => 20);
  104. sub track_id { refaddr $_[0] }
  105. sub buffer_ref {
  106. my $self = shift;
  107. return $BufferCache->get($self->track_id) || $BufferCache->set($self->track_id, \(my $s = ''));
  108. }
  109. sub buffer {
  110. my $self = shift;
  111. return ${ $self->buffer_ref };
  112. }
  113. sub append_buffer {
  114. my ($self, $buf) = @_;
  115. my $ref = $self->buffer_ref;
  116. $$ref .= $buf;
  117. }
  118. sub buffer_length {
  119. my $self = shift;
  120. return length $self->buffer;
  121. }
  122. sub has_buffer {
  123. my $self = shift;
  124. return !! $BufferCache->peek($self->track_id);
  125. }
  126. sub peek_buffer_length {
  127. my $self = shift;
  128. my $buffer_ref = $BufferCache->peek($self->track_id) or return 0;
  129. return length $$buffer_ref;
  130. }
  131. ### Instantiation
  132. our $UrlToInstance;
  133. our $IdToInstance;
  134. sub BUILD {
  135. my $self = shift;
  136. weaken($UrlToInstance->{ $self->url } = $self);
  137. weaken($IdToInstance->{ $self->track_id } = $self);
  138. }
  139. my @subclasses;
  140. sub subclasses {
  141. my $class = shift;
  142. return @subclasses if @subclasses;
  143. file(__FILE__)->dir->subdir('Track')->recurse(
  144. callback => sub {
  145. my $pm = shift;
  146. $pm = $pm->relative(file(__FILE__)->parent->parent);
  147. $pm =~ s/\.pm$// or return;
  148. $pm =~ s/\//::/g;
  149. Class::Load::load_class($pm);
  150. return unless $pm->meta->get_method('_play');
  151. push @subclasses, $pm;
  152. },
  153. );
  154. return @subclasses;
  155. }
  156. # below does not create instance
  157. sub of_url {
  158. my ($class, $url) = @_;
  159. return $UrlToInstance->{$url};
  160. }
  161. sub of_track_id {
  162. my ($class, $id) = @_;
  163. return $IdToInstance->{ $id };
  164. }
  165. sub from_url {
  166. my ($class, $url, %args) = @_;
  167. if (my $track = $class->of_url($url)) {
  168. return $track;
  169. }
  170. foreach my $impl ($class->subclasses) {
  171. my $args = $impl->buildargs_from_url($url) or next;
  172. return $impl->new(url => $url, %$args, %args);
  173. }
  174. }
  175. ### Subclasses must implement these
  176. sub buildargs_from_url {
  177. my $class = shift;
  178. die 'override';
  179. }
  180. sub _play {
  181. my $self = shift;
  182. die 'override';
  183. }
  184. ###
  185. sub add_error {
  186. my ($self, $error) = @_;
  187. $self->log(error => $error);
  188. $self->{error} = $self->{error} ? "$self->{error}; $error" : $error;
  189. }
  190. sub is_track_url {
  191. my ($class, $url) = @_;
  192. foreach my $impl ($class->subclasses) {
  193. $impl->buildargs_from_url($url) and return 1;
  194. }
  195. return 0;
  196. }
  197. sub is_system { 0 }
  198. sub log_extra_info {
  199. my $self = shift;
  200. return $self->url->path_query;
  201. }
  202. sub prepare {
  203. my $self = shift;
  204. $self->log(debug => 'prepare');
  205. $self->media_url; # build
  206. }
  207. sub play {
  208. my $self = shift;
  209. if ($self->is_playing) {
  210. $self->log(debug => 'already playing');
  211. return $self->error ? 0 : 1;
  212. } elsif ($self->is_done) {
  213. $self->log(debug => 'already done');
  214. return $self->error ? 0 : 1;
  215. }
  216. $self->log(info => 'start playing');
  217. $self->status(TRACK_STATUS_PLAYING);
  218. $self->buffer; # initialize
  219. $self->_play;
  220. if ($self->error) {
  221. $self->done;
  222. return 0;
  223. }
  224. return 1;
  225. }
  226. sub write {
  227. my $self = shift;
  228. $self->append_buffer($_[0]);
  229. # $self->log(debug => 'broadcast buffer_signal');
  230. $self->buffer_signal->broadcast;
  231. }
  232. sub buffer_read_fh {
  233. my $self = shift;
  234. open my $fh, '<', \$self->{buffer};
  235. }
  236. ### Utility methods
  237. sub recv_cv {
  238. my ($self, $cv) = @_;
  239. $cv->cb(Coro::rouse_cb);
  240. Coro::rouse_wait;
  241. return $cv->recv;
  242. }
  243. sub sleep {
  244. my ($self, $n) = @_;
  245. $self->log(info => "sleep for $n secs");
  246. Coro::Timer::sleep $n;
  247. }
  248. sub run_command {
  249. my ($self, $command, $args) = @_;
  250. my $head = $command->[0];
  251. $args ||= {};
  252. $args->{'>'} ||= sub {
  253. $self->log(debug => "$head: STDOUT: $_[0]") if defined $_[0];
  254. };
  255. $args->{'2>'} ||= sub {
  256. $self->log(debug => "$head: STDERR: $_[0]") if defined $_[0];
  257. };
  258. $args->{'$$'} = \my $pid;
  259. $self->log(debug => qq(running '@$command'));
  260. my $cmd_cv = run_cmd $command, %{ $args || {} };
  261. $self->log(debug => "$head: pid=$pid");
  262. my $exit_code = $self->recv_cv($cmd_cv);
  263. if ($exit_code != 0) {
  264. $self->add_error("$head exited with code $exit_code");
  265. } else {
  266. $self->log(debug => "$head exited with code $exit_code");
  267. }
  268. return $exit_code;
  269. }
  270. # transcode file or fh to buffer
  271. sub ffmpeg {
  272. my ($self, $file_or_fh) = @_;
  273. my %args = (
  274. '>' => unblock_sub { $self->write($_[0]) if defined $_[0] },
  275. '2>' => sub { $self->log_coro("ffmpeg: @_") },
  276. );
  277. my $filename;
  278. if (ref $file_or_fh) {
  279. $filename = '-';
  280. $args{'<'} = $file_or_fh;
  281. } else {
  282. $filename = $file_or_fh;
  283. }
  284. $self->run_command(
  285. [ qw(ffmpeg -i), $filename, qw(-ab 192k -ar 44100 -acodec libmp3lame -ac 2 -f mp3 -) ],
  286. \%args,
  287. );
  288. $self->done;
  289. }
  290. sub url_to_fh {
  291. my ($self, $url, %args) = @_;
  292. my $cb = delete $args{cb};
  293. my ($reader, $writer) = portable_pipe;
  294. my $write_handle = AnyEvent::Handle->new(
  295. fh => $writer,
  296. on_error => sub {
  297. my ($handle, $fatal, $msg) = @_;
  298. $self->add_error("AnyEvent::Handle: $msg");
  299. $handle->destroy;
  300. }
  301. );
  302. $self->log(debug => "GET $url");
  303. my $bytes_wrote = 0;
  304. http_get(
  305. $url,
  306. headers => $self->prepare_headers($url),
  307. on_header => sub {
  308. my ($headers) = @_;
  309. if ($headers->{Status} != 200) {
  310. $self->add_error("http_get $url: $headers->{Status} $headers->{Reason}");
  311. return;
  312. }
  313. 1;
  314. },
  315. on_body => sub {
  316. my ($content) = @_;
  317. if (defined $content) {
  318. $write_handle->push_write($content);
  319. $bytes_wrote += length $content;
  320. }
  321. 1;
  322. },
  323. sub {
  324. $write_handle->on_drain(sub { close $_[0]->fh; $_[0]->destroy });
  325. $self->log(info => "GET $url -> $bytes_wrote bytes");
  326. $cb && $cb->();
  327. },
  328. );
  329. return $reader;
  330. }
  331. sub prepare_headers {
  332. my ($self, $url) = @_;
  333. my %headers = (
  334. 'Referer' => undef,
  335. 'User-Agent' => $self->user_agent->agent,
  336. );
  337. $self->user_agent->prepare_request(GET $url)->scan(sub { $headers{$_[0]} = $_[1] });
  338. return \%headers;
  339. }
  340. sub tempfile {
  341. my $self = shift;
  342. return File::Temp::tempfile(UNLINK => 0, @_);
  343. }
  344. sub temporary_filename {
  345. my $self = shift;
  346. my $suffix = shift;
  347. my (undef, $filename) = $self->tempfile(OPEN => 0, SUFFIX => $suffix, @_);
  348. return $filename;
  349. }
  350. sub download_temporary {
  351. my ($self, $url, $suffix) = @_;
  352. my $filename = $self->temporary_filename($suffix);
  353. my $res = $self->user_agent->mirror($url, $filename);
  354. unless ($res->is_success) {
  355. $self->add_error("mirroring $url to $filename: " . $res->status_line);
  356. return undef;
  357. }
  358. return $filename;
  359. }
  360. sub send_file_to_buffer {
  361. my ($self, $file) = @_;
  362. my $fh = ref $file ? $file : aio_open $file, IO::AIO::O_RDONLY, 0 or do {
  363. $self->add_error("aio_open $file: $!");
  364. return;
  365. };
  366. while (aio_read $fh, undef, 1024 * 1024, my $buf = '', 0) {
  367. $self->write($buf);
  368. }
  369. aio_close $fh;
  370. $self->done;
  371. }
  372. sub as_string {
  373. my $self = shift;
  374. my $string = "<$self->{url}>";
  375. $string = $self->title . ' ' . $string if $self->title;
  376. return $string;
  377. }
  378. 1;