diff options
-rw-r--r-- | AUTHORS | 2 | ||||
-rw-r--r-- | COPYING | 340 | ||||
-rw-r--r-- | ChangeLog | 0 | ||||
-rw-r--r-- | Makefile.am | 38 | ||||
-rw-r--r-- | NEWS | 0 | ||||
-rw-r--r-- | README | 4 | ||||
-rw-r--r-- | TODO | 34 | ||||
-rw-r--r-- | audio-player.c | 984 | ||||
-rw-r--r-- | audio-player.h | 121 | ||||
-rw-r--r-- | configure.ac | 16 | ||||
-rw-r--r-- | gaku.desktop | 11 | ||||
-rw-r--r-- | main.c | 895 | ||||
-rw-r--r-- | marshal.list | 1 | ||||
-rw-r--r-- | mockup.py | 70 | ||||
-rw-r--r-- | playlist-parser.c | 320 | ||||
-rw-r--r-- | playlist-parser.h | 95 | ||||
-rw-r--r-- | tag-reader.c | 649 | ||||
-rw-r--r-- | tag-reader.h | 100 |
18 files changed, 3680 insertions, 0 deletions
@@ -0,0 +1,2 @@ +Jorn Baayen <jorn@openedhand.com> +Ross Burton <ross@openedhand.com> @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ChangeLog diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..af740ff --- /dev/null +++ b/Makefile.am @@ -0,0 +1,38 @@ +AUTOMAKE_OPTIONS = -Wno-portability + +bin_PROGRAMS = gaku + +AM_CPPFLAGS = $(DEPS_CFLAGS) +AM_CFLAGS = -Wall +LDADD = $(DEPS_LIBS) + +gaku_SOURCES = \ + main.c \ + audio-player.c audio-player.h \ + playlist-parser.c playlist-parser.h \ + tag-reader.c tag-reader.h \ + marshal.c marshal.h + +%.h: %.list + $(GLIB_GENMARSHAL) --prefix=$* $< --header > $@ + +%.c: %.list + $(GLIB_GENMARSHAL) --prefix=$* $< --body > $@ + +EXTRA_DIST = marshal.list +BUILT_SOURCES = marshal.c marshal.h +CLEANFILES = $(BUILT_SOURCES) + +desktopdir = $(datadir)/applications +dist_desktop_DATA = gaku.desktop + +MAINTAINERCLEANFILES = \ + aclocal.m4 \ + compile \ + config.guess \ + config.h.in \ + configure \ + depcomp \ + install-sh \ + Makefile.in \ + missing @@ -0,0 +1,4 @@ +Gaku Raku +=== + +A simple music player, using GTK+ and GStreamer. @@ -0,0 +1,34 @@ +Metadata panel between buttons and playlist +=== + ++-----+ Song Title +|cover| Artist ++-----+ Progress/Duration + +Some form of button or something to turn the progress display into a seek bar + + +Menu bar +=== + +Playlist +- Open +- Save +- --- +- Remove +- Clear +- --- +- Repeat +- Shuffle + + +Should the Add button be a combo menu button like on toolbars, for selecting +between add song/album/playlist when we've got a library. + +Volume control + +Probably ifdef for maemo, include playbinmaemo source. +http://gmyth.svn.sourceforge.net/viewvc/gmyth/trunk/gst-gmyth/playbinmaemo/gstplaybinmaemo.c + + +Rename to uta - "song" diff --git a/audio-player.c b/audio-player.c new file mode 100644 index 0000000..0c32e58 --- /dev/null +++ b/audio-player.c @@ -0,0 +1,984 @@ +/* + * Copyright (C) 2006 OpenedHand Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: Jorn Baayen <jorn@openedhand.com> + */ + +#include <gst/gst.h> + +#include "audio-player.h" + +G_DEFINE_TYPE (AudioPlayer, + audio_player, + G_TYPE_OBJECT); + +struct _AudioPlayerPrivate { + GstElement *playbin; + + char *uri; + + gboolean can_seek; + + int buffer_percent; + + int duration; + + guint tick_timeout_id; +}; + +enum { + PROP_0, + PROP_URI, + PROP_PLAYING, + PROP_POSITION, + PROP_VOLUME, + PROP_CAN_SEEK, + PROP_BUFFER_PERCENT, + PROP_DURATION +}; + +enum { + TAG_LIST_AVAILABLE, + EOS, + ERROR, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +#define TICK_TIMEOUT 0.5 + +/* TODO: Possibly retrieve these through introspection. The problem is that we + * need them in class_init already. */ +#define GST_VOL_DEFAULT 1.0 +#define GST_VOL_MAX 4.0 + +/** + * An error occured. + **/ +static void +bus_message_error_cb (GstBus *bus, + GstMessage *message, + AudioPlayer *audio_player) +{ + GError *error; + + error = NULL; + gst_message_parse_error (message, + &error, + NULL); + + g_signal_emit (audio_player, + signals[ERROR], + 0, + error); + + g_error_free (error); + +} + +/** + * End of stream reached. + **/ +static void +bus_message_eos_cb (GstBus *bus, + GstMessage *message, + AudioPlayer *audio_player) +{ + /** + * Make sure UI is in sync. + **/ + g_object_notify (G_OBJECT (audio_player), "position"); + + /** + * Emit EOS signal. + **/ + g_signal_emit (audio_player, + signals[EOS], + 0); +} + +/** + * Tag list available. + **/ +static void +bus_message_tag_cb (GstBus *bus, + GstMessage *message, + AudioPlayer *audio_player) +{ + GstTagList *tag_list; + + gst_message_parse_tag (message, &tag_list); + + g_signal_emit (audio_player, + signals[TAG_LIST_AVAILABLE], + 0, + tag_list); + + gst_tag_list_free (tag_list); +} + +/** + * Buffering information available. + **/ +static void +bus_message_buffering_cb (GstBus *bus, + GstMessage *message, + AudioPlayer *audio_player) +{ + const GstStructure *str; + + str = gst_message_get_structure (message); + if (!str) + return; + + if (!gst_structure_get_int (str, + "buffer-percent", + &audio_player->priv->buffer_percent)) + return; + + g_object_notify (G_OBJECT (audio_player), "buffer-percent"); +} + +/** + * Duration information available. + **/ +static void +bus_message_duration_cb (GstBus *bus, + GstMessage *message, + AudioPlayer *audio_player) +{ + GstFormat format; + gint64 duration; + + gst_message_parse_duration (message, + &format, + &duration); + + if (format != GST_FORMAT_TIME) + return; + + audio_player->priv->duration = duration / GST_SECOND; + + g_object_notify (G_OBJECT (audio_player), "duration"); +} + +/** + * A state change occured. + **/ +static void +bus_message_state_change_cb (GstBus *bus, + GstMessage *message, + AudioPlayer *audio_player) +{ + gpointer src; + GstState old_state, new_state; + + src = GST_MESSAGE_SRC (message); + + if (src != audio_player->priv->playbin) + return; + + gst_message_parse_state_changed (message, + &old_state, + &new_state, + NULL); + + if (old_state == GST_STATE_READY && + new_state == GST_STATE_PAUSED) { + GstQuery *query; + + /** + * Determine whether we can seek. + **/ + query = gst_query_new_seeking (GST_FORMAT_TIME); + + if (gst_element_query (audio_player->priv->playbin, query)) { + gst_query_parse_seeking (query, + NULL, + &audio_player->priv->can_seek, + NULL, + NULL); + } else { + /** + * Could not query for ability to seek. Determine + * using URI. + **/ + + if (g_str_has_prefix (audio_player->priv->uri, + "http://")) { + audio_player->priv->can_seek = FALSE; + } else { + audio_player->priv->can_seek = TRUE; + } + } + + gst_query_unref (query); + + g_object_notify (G_OBJECT (audio_player), "can-seek"); + + /** + * Determine the duration. + **/ + query = gst_query_new_duration (GST_FORMAT_TIME); + + if (gst_element_query (audio_player->priv->playbin, query)) { + gint64 duration; + + gst_query_parse_duration (query, + NULL, + &duration); + + audio_player->priv->duration = duration / GST_SECOND; + + g_object_notify (G_OBJECT (audio_player), "duration"); + } + + gst_query_unref (query); + } +} + +/** + * Called every TICK_TIMEOUT secs to notify of a position change. + **/ +static gboolean +tick_timeout (AudioPlayer *audio_player) +{ + g_object_notify (G_OBJECT (audio_player), "position"); + + return TRUE; +} + +/** + * Constructs the GStreamer pipeline. + **/ +static void +construct_pipeline (AudioPlayer *audio_player) +{ + + GstElement *audiosink; + GstBus *bus; + + /** + * playbin. + **/ + audio_player->priv->playbin = + gst_element_factory_make ("playbin", "playbin"); + if (!audio_player->priv->playbin) { + g_warning ("No playbin found. Playback will not work."); + + return; + } + + /** + * An audiosink. + **/ + audiosink = gst_element_factory_make ("gconfaudiosink", "audiosink"); + if (!audiosink) { + g_warning ("No gconfaudiosink found. Trying autoaudiosink ..."); + + audiosink = gst_element_factory_make ("autoaudiosink", + "audiosink"); + if (!audiosink) { + g_warning ("No autoaudiosink found. " + "Trying alsasink ..."); + + audiosink = gst_element_factory_make ("alsasink", + "audiosink"); + if (!audiosink) { + g_warning ("No audiosink could be found. " + "Audio will not be available."); + } + } + } + + /** + * Click sinks into playbin. + **/ + g_object_set (G_OBJECT (audio_player->priv->playbin), + "audio-sink", audiosink, + "video-sink", NULL, + NULL); + + /** + * Connect to signals on bus. + **/ + bus = gst_pipeline_get_bus (GST_PIPELINE (audio_player->priv->playbin)); + + gst_bus_add_signal_watch (bus); + + g_signal_connect_object (bus, + "message::error", + G_CALLBACK (bus_message_error_cb), + audio_player, + 0); + g_signal_connect_object (bus, + "message::eos", + G_CALLBACK (bus_message_eos_cb), + audio_player, + 0); + g_signal_connect_object (bus, + "message::tag", + G_CALLBACK (bus_message_tag_cb), + audio_player, + 0); + g_signal_connect_object (bus, + "message::buffering", + G_CALLBACK (bus_message_buffering_cb), + audio_player, + 0); + g_signal_connect_object (bus, + "message::duration", + G_CALLBACK (bus_message_duration_cb), + audio_player, + 0); + g_signal_connect_object (bus, + "message::state-changed", + G_CALLBACK (bus_message_state_change_cb), + audio_player, + 0); + + gst_object_unref (GST_OBJECT (bus)); +} + +static void +audio_player_init (AudioPlayer *audio_player) +{ + /** + * Create pointer to private data. + **/ + audio_player->priv = + G_TYPE_INSTANCE_GET_PRIVATE (audio_player, + TYPE_AUDIO_PLAYER, + AudioPlayerPrivate); + + /** + * Construct GStreamer pipeline: playbin with sinks from GConf. + **/ + construct_pipeline (audio_player); +} + +static void +audio_player_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + AudioPlayer *audio_player; + + audio_player = AUDIO_PLAYER (object); + + switch (property_id) { + case PROP_URI: + audio_player_set_uri (audio_player, + g_value_get_string (value)); + break; + case PROP_PLAYING: + audio_player_set_playing (audio_player, + g_value_get_boolean (value)); + break; + case PROP_POSITION: + audio_player_set_position (audio_player, + g_value_get_int (value)); + break; + case PROP_VOLUME: + audio_player_set_volume (audio_player, + g_value_get_double (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +audio_player_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + AudioPlayer *audio_player; + + audio_player = AUDIO_PLAYER (object); + + switch (property_id) { + case PROP_URI: + g_value_set_string + (value, + audio_player_get_uri (audio_player)); + break; + case PROP_PLAYING: + g_value_set_boolean + (value, + audio_player_get_playing (audio_player)); + break; + case PROP_POSITION: + g_value_set_int + (value, + audio_player_get_position (audio_player)); + break; + case PROP_VOLUME: + g_value_set_double + (value, + audio_player_get_volume (audio_player)); + break; + case PROP_CAN_SEEK: + g_value_set_boolean + (value, + audio_player_get_can_seek (audio_player)); + break; + case PROP_BUFFER_PERCENT: + g_value_set_int + (value, + audio_player_get_buffer_percent (audio_player)); + break; + case PROP_DURATION: + g_value_set_int + (value, + audio_player_get_duration (audio_player)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +audio_player_dispose (GObject *object) +{ + AudioPlayer *audio_player; + GObjectClass *object_class; + + audio_player = AUDIO_PLAYER (object); + + if (audio_player->priv->playbin) { + gst_element_set_state (audio_player->priv->playbin, + GST_STATE_NULL); + + gst_object_unref (GST_OBJECT (audio_player->priv->playbin)); + audio_player->priv->playbin = NULL; + } + + if (audio_player->priv->tick_timeout_id > 0) { + g_source_remove (audio_player->priv->tick_timeout_id); + audio_player->priv->tick_timeout_id = 0; + } + + object_class = G_OBJECT_CLASS (audio_player_parent_class); + object_class->dispose (object); +} + +static void +audio_player_finalize (GObject *object) +{ + AudioPlayer *audio_player; + GObjectClass *object_class; + + audio_player = AUDIO_PLAYER (object); + + g_free (audio_player->priv->uri); + + object_class = G_OBJECT_CLASS (audio_player_parent_class); + object_class->finalize (object); +} + +static void +audio_player_class_init (AudioPlayerClass *klass) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = audio_player_set_property; + object_class->get_property = audio_player_get_property; + object_class->dispose = audio_player_dispose; + object_class->finalize = audio_player_finalize; + + g_type_class_add_private (klass, sizeof (AudioPlayerPrivate)); + + g_object_class_install_property + (object_class, + PROP_URI, + g_param_spec_string + ("uri", + "URI", + "The loaded URI.", + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB)); + + g_object_class_install_property + (object_class, + PROP_PLAYING, + g_param_spec_boolean + ("playing", + "Playing", + "TRUE if playing.", + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB)); + + g_object_class_install_property + (object_class, + PROP_POSITION, + g_param_spec_int + ("position", + "Position", + "The position in the current stream in seconds.", + 0, G_MAXINT, 0, + G_PARAM_READWRITE | + G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB)); + + g_object_class_install_property + (object_class, + PROP_VOLUME, + g_param_spec_double + ("volume", + "Volume", + "The audio volume.", + 0, GST_VOL_MAX, GST_VOL_DEFAULT, + G_PARAM_READWRITE | + G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB)); + + g_object_class_install_property + (object_class, + PROP_CAN_SEEK, + g_param_spec_boolean + ("can-seek", + "Can seek", + "TRUE if the current stream is seekable.", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB)); + + g_object_class_install_property + (object_class, + PROP_BUFFER_PERCENT, + g_param_spec_int + ("buffer-percent", + "Buffer percent", + "The percentage the current stream buffer is filled.", + 0, 100, 0, + G_PARAM_READABLE | + G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB)); + + g_object_class_install_property + (object_class, + PROP_DURATION, + g_param_spec_int + ("duration", + "Duration", + "The duration of the current stream in seconds.", + 0, G_MAXINT, 0, + G_PARAM_READABLE | + G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | + G_PARAM_STATIC_BLURB)); + + signals[TAG_LIST_AVAILABLE] = + g_signal_new ("tag-list-available", + TYPE_AUDIO_PLAYER, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (AudioPlayerClass, + tag_list_available), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, G_TYPE_POINTER); + + signals[EOS] = + g_signal_new ("eos", + TYPE_AUDIO_PLAYER, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (AudioPlayerClass, + eos), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[ERROR] = + g_signal_new ("error", + TYPE_AUDIO_PLAYER, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (AudioPlayerClass, + error), + NULL, NULL, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, G_TYPE_POINTER); +} + +/** + * audio_player_new + * + * Return value: A new #AudioPlayer. + **/ +AudioPlayer * +audio_player_new (void) +{ + return g_object_new (TYPE_AUDIO_PLAYER, NULL); +} + +/** + * audio_player_set_uri + * @audio_player: A #AudioPlayer + * @uri: A URI + * + * Loads @uri. + **/ +void +audio_player_set_uri (AudioPlayer *audio_player, + const char *uri) +{ + GstState state, pending; + + g_return_if_fail (IS_AUDIO_PLAYER (audio_player)); + + if (!audio_player->priv->playbin) + return; + + g_free (audio_player->priv->uri); + + if (uri) { + audio_player->priv->uri = g_strdup (uri); + + /** + * Ensure the tick timeout is installed. + * + * We also have it installed in PAUSED state, because + * seeks etc may have a delayed effect on the position. + **/ + if (audio_player->priv->tick_timeout_id == 0) { + audio_player->priv->tick_timeout_id = + g_timeout_add (TICK_TIMEOUT * 1000, + (GSourceFunc) tick_timeout, + audio_player); + } + } else { + audio_player->priv->uri = NULL; + + /** + * Remove tick timeout. + **/ + if (audio_player->priv->tick_timeout_id > 0) { + g_source_remove (audio_player->priv->tick_timeout_id); + audio_player->priv->tick_timeout_id = 0; + } + } + + /** + * Reset properties. + **/ + audio_player->priv->can_seek = FALSE; + audio_player->priv->duration = 0; + + /** + * Store old state. + **/ + gst_element_get_state (audio_player->priv->playbin, + &state, + &pending, + 0); + if (pending) + state = pending; + + /** + * State to NULL. + **/ + gst_element_set_state (audio_player->priv->playbin, GST_STATE_NULL); + + /** + * Set new URI. + **/ + g_object_set (audio_player->priv->playbin, + "uri", uri, + NULL); + + /** + * Restore state. + **/ + if (uri) + gst_element_set_state (audio_player->priv->playbin, state); + + /** + * Emit notififications for all these to make sure UI is not showing + * any properties of the old URI. + **/ + g_object_notify (G_OBJECT (audio_player), "uri"); + g_object_notify (G_OBJECT (audio_player), "can-seek"); + g_object_notify (G_OBJECT (audio_player), "duration"); + g_object_notify (G_OBJECT (audio_player), "position"); +} + +/** + * audio_player_get_uri + * @audio_player: A #AudioPlayer + * + * Return value: The loaded URI, or NULL if none set. + **/ +const char * +audio_player_get_uri (AudioPlayer *audio_player) +{ + g_return_val_if_fail (IS_AUDIO_PLAYER (audio_player), NULL); + + return audio_player->priv->uri; +} + +/** + * audio_player_set_playing + * @audio_player: A #AudioPlayer + * @playing: TRUE if @audio_player should be playing, FALSE otherwise + * + * Sets the playback state of @audio_player to @playing. + **/ +void +audio_player_set_playing (AudioPlayer *audio_player, + gboolean playing) +{ + g_return_if_fail (IS_AUDIO_PLAYER (audio_player)); + + if (!audio_player->priv->playbin) + return; + + /** + * Choose the correct state for the pipeline. + **/ + if (audio_player->priv->uri) { + GstState state; + + if (playing) + state = GST_STATE_PLAYING; + else + state = GST_STATE_PAUSED; + + gst_element_set_state (audio_player->priv->playbin, state); + } else { + if (playing) + g_warning ("Tried to play, but no URI is loaded."); + + /** + * Do nothing. + **/ + } + + g_object_notify (G_OBJECT (audio_player), "playing"); + + /** + * Make sure UI is in sync. + **/ + g_object_notify (G_OBJECT (audio_player), "position"); +} + +/** + * audio_player_get_playing + * @audio_player: A #AudioPlayer + * + * Return value: TRUE if @audio_player is playing. + **/ +gboolean +audio_player_get_playing (AudioPlayer *audio_player) +{ + GstState state, pending; + + g_return_val_if_fail (IS_AUDIO_PLAYER (audio_player), FALSE); + + if (!audio_player->priv->playbin) + return FALSE; + + gst_element_get_state (audio_player->priv->playbin, + &state, + &pending, + 0); + + if (pending) + return (pending == GST_STATE_PLAYING); + else + return (state == GST_STATE_PLAYING); +} + +/** + * audio_player_set_position + * @audio_player: A #AudioPlayer + * @position: The position in the current stream in seconds. + * + * Sets the position in the current stream to @position. + **/ +void +audio_player_set_position (AudioPlayer *audio_player, + int position) +{ + GstState state, pending; + + g_return_if_fail (IS_AUDIO_PLAYER (audio_player)); + + if (!audio_player->priv->playbin) + return; + + /** + * Store old state. + **/ + gst_element_get_state (audio_player->priv->playbin, + &state, + &pending, + 0); + if (pending) + state = pending; + + /** + * State to PAUSED. + **/ + gst_element_set_state (audio_player->priv->playbin, GST_STATE_PAUSED); + + /** + * Perform the seek. + **/ + gst_element_seek (audio_player->priv->playbin, + 1.0, + GST_FORMAT_TIME, + GST_SEEK_FLAG_FLUSH, + GST_SEEK_TYPE_SET, + position * GST_SECOND, + 0, 0); + /** + * Restore state. + **/ + gst_element_set_state (audio_player->priv->playbin, state); +} + +/** + * audio_player_get_position + * @audio_player: A #AudioPlayer + * + * Return value: The position in the current file in seconds. + **/ +int +audio_player_get_position (AudioPlayer *audio_player) +{ + GstQuery *query; + gint64 position; + + g_return_val_if_fail (IS_AUDIO_PLAYER (audio_player), -1); + + if (!audio_player->priv->playbin) + return -1; + + query = gst_query_new_position (GST_FORMAT_TIME); + + if (gst_element_query (audio_player->priv->playbin, query)) { + gst_query_parse_position (query, + NULL, + &position); + } else + position = 0; + + gst_query_unref (query); + + return (position / GST_SECOND); +} + +/** + * audio_player_set_volume + * @audio_player: A #AudioPlayer + * @volume: The audio volume to set, in the range 0.0 - 4.0. + * + * Sets the current audio volume to @volume. + **/ +void +audio_player_set_volume (AudioPlayer *audio_player, + double volume) +{ + g_return_if_fail (IS_AUDIO_PLAYER (audio_player)); + g_return_if_fail (volume >= 0.0 && volume <= GST_VOL_MAX); + + if (!audio_player->priv->playbin) + return; + + g_object_set (G_OBJECT (audio_player->priv->playbin), + "volume", volume, + NULL); + + g_object_notify (G_OBJECT (audio_player), "volume"); +} + +/** + * audio_player_get_volume + * @audio_player: A #AudioPlayer + * + * Return value: The current audio volume, in the range 0.0 - 4.0. + **/ +double +audio_player_get_volume (AudioPlayer *audio_player) +{ + double volume; + + g_return_val_if_fail (IS_AUDIO_PLAYER (audio_player), 0); + + if (!audio_player->priv->playbin) + return 0.0; + + g_object_get (audio_player->priv->playbin, + "volume", &volume, + NULL); + + return volume; +} + +/** + * audio_player_get_can_seek + * @audio_player: A #AudioPlayer + * + * Return value: TRUE if the current stream is seekable. + **/ +gboolean +audio_player_get_can_seek (AudioPlayer *audio_player) +{ + g_return_val_if_fail (IS_AUDIO_PLAYER (audio_player), FALSE); + + return audio_player->priv->can_seek; +} + +/** + * audio_player_get_buffer_percent + * @audio_player: A #AudioPlayer + * + * Return value: Percentage the current stream buffer is filled. + **/ +int +audio_player_get_buffer_percent (AudioPlayer *audio_player) +{ + g_return_val_if_fail (IS_AUDIO_PLAYER (audio_player), -1); + + return audio_player->priv->buffer_percent; +} + +/** + * audio_player_get_duration + * @audio_player: A #AudioPlayer + * + * Return value: The duration of the current stream in seconds. + **/ +int +audio_player_get_duration (AudioPlayer *audio_player) +{ + g_return_val_if_fail (IS_AUDIO_PLAYER (audio_player), -1); + + return audio_player->priv->duration; +} diff --git a/audio-player.h b/audio-player.h new file mode 100644 index 0000000..d57e222 --- /dev/null +++ b/audio-player.h @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2006 OpenedHand Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: Jorn Baayen <jorn@openedhand.com> + */ + +#ifndef __AUDIO_PLAYER_H__ +#define __AUDIO_PLAYER_H__ + +#include <glib-object.h> +#include <gst/gsttaglist.h> + +G_BEGIN_DECLS + +#define TYPE_AUDIO_PLAYER \ + (audio_player_get_type ()) +#define AUDIO_PLAYER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), \ + TYPE_AUDIO_PLAYER, \ + AudioPlayer)) +#define AUDIO_PLAYER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), \ + TYPE_AUDIO_PLAYER, \ + AudioPlayerClass)) +#define IS_AUDIO_PLAYER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \ + TYPE_AUDIO_PLAYER)) +#define IS_AUDIO_PLAYER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), \ + TYPE_AUDIO_PLAYER)) +#define AUDIO_PLAYER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), \ + TYPE_AUDIO_PLAYER, \ + AudioPlayerClass)) + +typedef struct _AudioPlayerPrivate AudioPlayerPrivate; + +typedef struct { + GObject parent; + + AudioPlayerPrivate *priv; +} AudioPlayer; + +typedef struct { + GObjectClass parent_class; + + /* Signals */ + void (* tag_list_available) (AudioPlayer *audio_player, + GstTagList *tag_list); + void (* eos) (AudioPlayer *audio_player); + void (* error) (AudioPlayer *audio_player, + GError *error); + + /* Future padding */ + void (* _reserved1) (void); + void (* _reserved2) (void); + void (* _reserved3) (void); + void (* _reserved4) (void); +} AudioPlayerClass; + +GType +audio_player_get_type (void) G_GNUC_CONST; + +AudioPlayer * +audio_player_new (void); + +void +audio_player_set_uri (AudioPlayer *audio_player, + const char *uri); + +const char * +audio_player_get_uri (AudioPlayer *audio_player); + +void +audio_player_set_playing (AudioPlayer *audio_player, + gboolean playing); + +gboolean +audio_player_get_playing (AudioPlayer *audio_player); + +void +audio_player_set_position (AudioPlayer *audio_player, + int position); + +int +audio_player_get_position (AudioPlayer *audio_player); + +void +audio_player_set_volume (AudioPlayer *audio_player, + double volume); + +double +audio_player_get_volume (AudioPlayer *audio_player); + +gboolean +audio_player_get_can_seek (AudioPlayer *audio_player); + +int +audio_player_get_buffer_percent (AudioPlayer *audio_player); + +int +audio_player_get_duration (AudioPlayer *audio_player); + +G_END_DECLS + +#endif /* __AUDIO_PLAYER_H__ */ diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..db4d970 --- /dev/null +++ b/configure.ac @@ -0,0 +1,16 @@ +AC_PREREQ(2.52) +AC_INIT(gaku, 0.1, http://o-hand.com) +AC_CONFIG_SRCDIR(audio-player.c) +AM_INIT_AUTOMAKE(AC_PACKAGE_NAME, AC_PACKAGE_VERSION) +AM_CONFIG_HEADER(config.h) + +AM_DISABLE_STATIC +AC_PROG_CPP +AC_PROG_CC + +PKG_CHECK_MODULES(DEPS, gtk+-2.0 gstreamer-0.10) + +GLIB_GENMARSHAL=`$PKG_CONFIG --variable=glib_genmarshal glib-2.0` +AC_SUBST(GLIB_GENMARSHAL) + +AC_OUTPUT([Makefile]) diff --git a/gaku.desktop b/gaku.desktop new file mode 100644 index 0000000..43b1afb --- /dev/null +++ b/gaku.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Encoding=UTF-8 +Name=Music Player +Comment=Play your favourite songs +Icon=audio-player +TryExec=gaku +Exec=gaku +StartupNotify=true +Terminal=false +Type=Application +Categories=GNOME;GTK;AudioVideo;Audio;Player; @@ -0,0 +1,895 @@ +/* + * Copyright (C) 2006 OpenedHand Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: Jorn Baayen <jorn@openedhand.com> + */ + +#include <gst/gst.h> +#include <gtk/gtk.h> +#include <string.h> + +#include "audio-player.h" +#include "playlist-parser.h" +#include "tag-reader.h" + +typedef struct { + /** + * Our special objects. + **/ + AudioPlayer *audio_player; + PlaylistParser *playlist_parser; + TagReader *tag_reader; + + /** + * UI objects. + **/ + GtkWidget *window; + GtkWidget *play_pause_button; + GtkWidget *previous_button; + GtkWidget *next_button; + GtkWidget *tree_view; + + GtkListStore *list_store; + GtkTreeRowReference *playing_row; + + char *last_folder; +} AppData; + +enum { + COL_TITLE, + COL_ARTIST, + COL_URI +}; + +/** + * Returns TRUE if @iter is the currently playing row. + **/ +static gboolean +iter_is_playing_row (AppData *data, + GtkTreeIter *iter) +{ + GtkTreePath *path, *playing_path; + gboolean retval; + + if (!data->playing_row) + return FALSE; + + playing_path = gtk_tree_row_reference_get_path (data->playing_row); + if (playing_path == NULL) + /* The currently playing row has been deleted */ + return FALSE; + + path = gtk_tree_model_get_path (GTK_TREE_MODEL (data->list_store), + iter); + + retval = (gtk_tree_path_compare (path, playing_path) == 0); + + gtk_tree_path_free (playing_path); + gtk_tree_path_free (path); + + return retval; +} + +static void +update_title (AppData *data, const char *title) +{ + char *win_title; + + if (title) { + win_title = g_strdup_printf ("%s - Music Player", title); + gtk_window_set_title (GTK_WINDOW (data->window), win_title); + } else { + gtk_window_set_title (GTK_WINDOW (data->window), "Music Player"); + } +} + +/** + * Set @iter to be the playing row. + **/ +static void +set_playing_row (AppData *data, + GtkTreeIter *iter) +{ + GtkTreeModel *tree_model; + GtkTreePath *path; + + tree_model = GTK_TREE_MODEL (data->list_store); + + if (data->playing_row) { + GtkTreeIter playing_iter; + + path = gtk_tree_row_reference_get_path (data->playing_row); + + /** + * Free old playing row. + **/ + gtk_tree_row_reference_free (data->playing_row); + data->playing_row = NULL; + + /** + * Emit changed signal for old playing row. + **/ + gtk_tree_model_get_iter (tree_model, &playing_iter, path); + gtk_tree_model_row_changed (tree_model, path, &playing_iter); + } + + if (iter) { + char *uri, *title; + + path = gtk_tree_model_get_path (tree_model, iter); + + /** + * Create new playing row. + **/ + data->playing_row = gtk_tree_row_reference_new (tree_model, + path); + + /** + * Emit changed signal for new playing row. + **/ + gtk_tree_model_row_changed (tree_model, path, iter); + + gtk_tree_path_free (path); + + /** + * Get data off new playing row. + **/ + gtk_tree_model_get (tree_model, + iter, + COL_URI, &uri, + COL_TITLE, &title, + -1); + + audio_player_set_uri (data->audio_player, uri); + + update_title (data, title); + + /* TODO show song metadata */ + + g_free (uri); + g_free (title); + } else { + data->playing_row = NULL; + /** + * No playing row. Reset window title. + **/ + update_title (data, NULL); + /* TODO hide metadata */ + } +} + +/** + * Skip to the previous song. + **/ +static gboolean +previous (AppData *data) +{ + GtkTreePath *path; + GtkTreeIter iter; + + if (!data->playing_row) + return FALSE; + + path = gtk_tree_row_reference_get_path (data->playing_row); + if (!gtk_tree_path_prev (path)) { + gtk_tree_path_free (path); + + return FALSE; + } + + gtk_tree_model_get_iter (GTK_TREE_MODEL (data->list_store), + &iter, path); + gtk_tree_path_free (path); + + set_playing_row (data, &iter); + + return TRUE; +} + +/** + * Skip to the next song. + **/ +static gboolean +next (AppData *data) +{ + GtkTreeModel *tree_model; + GtkTreePath *path; + GtkTreeIter iter; + + if (!data->playing_row) + return FALSE; + + tree_model = GTK_TREE_MODEL (data->list_store); + + path = gtk_tree_row_reference_get_path (data->playing_row); + gtk_tree_model_get_iter (tree_model, &iter, path); + gtk_tree_path_free (path); + + if (!gtk_tree_model_iter_next (tree_model, &iter)) + return FALSE; + + set_playing_row (data, &iter); + + return TRUE; +} + +/** + * End of stream reached. + **/ +static void +eos_cb (AudioPlayer *player, + AppData *data) +{ + /** + * Go to next song. + **/ + next (data); +} + +/** + * We are loading a new playlist. + **/ +static void +playlist_start_cb (PlaylistParser *parser, + AppData *data) +{ + set_playing_row (data, NULL); + + /** + * Clear playlist. + **/ + gtk_list_store_clear (data->list_store); +} + +/** + * Add an URI to the playlist. + **/ +static void +add_uri (AppData *data, + const char *uri) +{ + GtkTreeIter iter; + char *filename, *basename; + + filename = g_filename_from_uri (uri, NULL, NULL); + if (!filename) + return; + + /** + * Display the file's basename by default. + **/ + basename = g_path_get_basename (filename); + g_free (filename); + + /** + * Add to playlist. + **/ + gtk_list_store_insert_with_values (data->list_store, + &iter, + -1, + COL_TITLE, basename, + COL_ARTIST, "", + COL_URI, uri, + -1); + + g_free (basename); + + /** + * Feed to tag reader. + **/ + tag_reader_scan_uri (data->tag_reader, uri); + + /** + * Play this song if nothing is playing. + **/ + if (!data->playing_row) { + set_playing_row (data, &iter); + + gtk_toggle_button_set_active + (GTK_TOGGLE_BUTTON (data->play_pause_button), TRUE); + } +} + +/** + * TagReader is done scanning an URI. Update UI. + **/ +static void +tag_reader_uri_scanned_cb (TagReader *tag_reader, + const char *uri, + GError *error, + GstTagList *tag_list, + AppData *data) +{ + GtkTreeModel *tree_model; + GtkTreeIter iter; + char *title, *artist; + + if (error) { + g_warning (error->message); + + return; + } + + if (!tag_list) + return; + + /** + * Find appropriate row(s). + * + * XXX For bigger playlists this is gonna be slow. Having a + * URI->iter(s) map would be faster if big playlists are a common + * case. + **/ + tree_model = GTK_TREE_MODEL (data->list_store); + + if (!gtk_tree_model_get_iter_first (tree_model, &iter)) + return; + + gst_tag_list_get_string (tag_list, + GST_TAG_TITLE, + &title); + gst_tag_list_get_string (tag_list, + GST_TAG_ARTIST, + &artist); + + do { + char *this_uri; + + gtk_tree_model_get (tree_model, + &iter, + COL_URI, + &this_uri, + -1); + + if (!strcmp (uri, this_uri)) { + /** + * Found a matching row. + **/ + gtk_list_store_set (data->list_store, + &iter, + COL_TITLE, title, + COL_ARTIST, artist, + -1); + + if (iter_is_playing_row (data, &iter)) { + /** + * This is the playing row as well. + * Update window title. + **/ + update_title (data, title); + } + } + + g_free (this_uri); + } while (gtk_tree_model_iter_next (tree_model, &iter)); + + g_free (title); + g_free (artist); +} + +/** + * Window deleted. Quit app. + **/ +static gboolean +window_delete_event_cb (GtkWidget *window, + GdkEvent *event, + gpointer user_data) +{ + gtk_main_quit (); + + return TRUE; +} + +/** + * 'Play/Pause' button clicked. + **/ +static void +play_pause_button_toggled_cb (GtkToggleButton *button, + AppData *data) +{ + audio_player_set_playing (data->audio_player, button->active); +} + +/** + * 'Previous' button clicked. + **/ +static void +previous_button_clicked_cb (GtkButton *button, + AppData *data) +{ + previous (data); +} + +/** + * 'Next' button clicked. + **/ +static void +next_button_clicked_cb (GtkButton *button, + AppData *data) +{ + next (data); +} + +#if 0 +/** + * 'Open playlist' button clicked. + **/ +static void +open_playlist_button_clicked_cb (GtkButton *button, + AppData *data) +{ + GtkWidget *dialog; + + dialog = gtk_file_chooser_dialog_new ("Open Playlist", + GTK_WINDOW (data->window), + GTK_FILE_CHOOSER_ACTION_OPEN, + GTK_STOCK_CANCEL, + GTK_RESPONSE_CANCEL, + GTK_STOCK_OPEN, + GTK_RESPONSE_ACCEPT, + NULL); + + switch (gtk_dialog_run (GTK_DIALOG (dialog))) { + default: + /* Fall through */ + case GTK_RESPONSE_CANCEL: + break; + case GTK_RESPONSE_ACCEPT: + { + char *uri; + GError *error; + + uri = gtk_file_chooser_get_uri (GTK_FILE_CHOOSER (dialog)); + + error = NULL; + if (!playlist_parser_parse (data->playlist_parser, + uri, &error)) { + g_warning (error->message); + + g_error_free (error); + } + + g_free (uri); + + break; + } + } + + gtk_widget_destroy (dialog); +} +#endif + +/** + * 'Add song' button clicked. + **/ +static void +add_song_button_clicked_cb (GtkButton *button, + AppData *data) +{ + GtkWidget *dialog; + GtkFileChooser *chooser; + + dialog = gtk_file_chooser_dialog_new ("Add Song", + GTK_WINDOW (data->window), + GTK_FILE_CHOOSER_ACTION_OPEN, + GTK_STOCK_CANCEL, + GTK_RESPONSE_CANCEL, + GTK_STOCK_OPEN, + GTK_RESPONSE_ACCEPT, + NULL); + chooser = GTK_FILE_CHOOSER (dialog); + + gtk_file_chooser_set_select_multiple (chooser, TRUE); + + if (data->last_folder) { + gtk_file_chooser_set_current_folder_uri (chooser, data->last_folder); + } + + switch (gtk_dialog_run (GTK_DIALOG (dialog))) { + default: + /* Fall through */ + case GTK_RESPONSE_CANCEL: + break; + case GTK_RESPONSE_ACCEPT: + { + GSList *uris; + + if (data->last_folder) { + g_free (data->last_folder); + } + data->last_folder = gtk_file_chooser_get_current_folder_uri (chooser); + + uris = gtk_file_chooser_get_uris (chooser); + + while (uris) { + char *uri = uris->data; + + add_uri (data, uri); + g_free (uri); + + uris = g_slist_delete_link (uris, uris); + } + break; + } + } + + gtk_widget_destroy (dialog); +} + +/** + * 'Remove song' button clicked. + **/ +static void +remove_song_button_clicked_cb (GtkButton *button, + AppData *data) +{ + GtkTreeModel *model = GTK_TREE_MODEL (data->list_store); + GtkTreeSelection *selection; + GList *rows, *l; + GtkTreeIter iter; + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (data->tree_view)); + + rows = gtk_tree_selection_get_selected_rows (selection, NULL); + + if (rows == NULL) + return; + + /* Convert the paths to row references */ + for (l = rows; l; l = l->next) { + GtkTreePath *path = l->data; + GtkTreeRowReference *ref; + + ref = gtk_tree_row_reference_new (model, path); + gtk_tree_path_free (path); + l->data = ref; + } + + /* Remove the rows */ + for (l = rows; l; l = l->next) { + GtkTreePath *path; + + path = gtk_tree_row_reference_get_path (l->data); + + gtk_tree_model_get_iter (model, &iter, path); + + /* If this song was playing, try and play the next song. */ + if (iter_is_playing_row (data, &iter)) { + GtkTreeIter iter2; + path = gtk_tree_path_copy (path); + gtk_tree_path_next (path); + if (gtk_tree_model_get_iter (model, &iter2, path)) { + set_playing_row (data, &iter2); + } else { + set_playing_row (data, NULL); + gtk_toggle_button_set_active + (GTK_TOGGLE_BUTTON (data->play_pause_button), FALSE); + } + gtk_tree_path_free (path); + } + + gtk_list_store_remove (data->list_store, &iter); + } + + g_list_foreach (rows, (GFunc)gtk_tree_row_reference_free, NULL); + g_list_free (rows); +} + +/** + * Tree view row activated. + **/ +static void +row_activated_cb (GtkTreeView *tree_view, + GtkTreePath *path, + GtkTreeViewColumn *column, + AppData *data) +{ + GtkTreeIter iter; + + gtk_tree_model_get_iter (GTK_TREE_MODEL (data->list_store), + &iter, + path); + + set_playing_row (data, &iter); +} + +/** + * 'Playing' column cell data function. + **/ +static void +playing_cell_func (GtkTreeViewColumn *col, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + AppData *data) +{ + const char *stock_id; + + if (iter_is_playing_row (data, iter)) + stock_id = GTK_STOCK_MEDIA_PLAY; + else + stock_id = NULL; + + g_object_set (cell, + "stock-size", GTK_ICON_SIZE_MENU, + "stock-id", stock_id, + NULL); +} + +/** + * Text column cell data function. + **/ +static void +text_cell_func (GtkTreeViewColumn *col, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer data) +{ + char *title, *artist, *text; + + gtk_tree_model_get (model, iter, + COL_TITLE, &title, + COL_ARTIST, &artist, + -1); + + text = g_markup_printf_escaped ("<b>%s</b>\n%s", title, artist); + + g_free (artist); + g_free (title); + + g_object_set (cell, "markup", text, NULL); + + g_free (text); +} + +/** + * Main. + **/ +int +main (int argc, char **argv) +{ + AppData *data; + GtkWidget *vbox, *hbox, *bbox, *scrolled_window; + GtkWidget *button, *image; + + /** + * Initialize APIs. + **/ + gst_init (&argc, &argv); + gtk_init (&argc, &argv); + + /** + * Create AppData structure. + **/ + data = g_slice_new0 (AppData); + + /** + * Set up AudioPlayer. + **/ + data->audio_player = audio_player_new (); + + g_signal_connect (data->audio_player, + "eos", + G_CALLBACK (eos_cb), + data); + + /** + * Set up PlaylistParser. + **/ + data->playlist_parser = playlist_parser_new (); + + g_signal_connect (data->playlist_parser, + "playlist-start", + G_CALLBACK (playlist_start_cb), + data); + + g_signal_connect_swapped (data->playlist_parser, + "entry", + G_CALLBACK (add_uri), + data); + + /** + * Set up TagReader. + **/ + data->tag_reader = tag_reader_new (); + + g_signal_connect (data->tag_reader, + "uri-scanned", + G_CALLBACK (tag_reader_uri_scanned_cb), + data); + + /** + * Create UI. + **/ + gtk_window_set_default_icon_name ("audio-player"); + + data->window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + + gtk_window_set_default_size (GTK_WINDOW (data->window), + 400, 500); + + gtk_container_set_border_width (GTK_CONTAINER (data->window), 4); + + g_signal_connect (data->window, + "delete-event", + G_CALLBACK (window_delete_event_cb), + data); + + vbox = gtk_vbox_new (FALSE, 6); + gtk_container_add (GTK_CONTAINER (data->window), vbox); + + hbox = gtk_hbox_new (FALSE, 4); + gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); + + data->play_pause_button = gtk_toggle_button_new (); + bbox = gtk_hbox_new (FALSE, 4); + image = gtk_image_new_from_stock (GTK_STOCK_MEDIA_PLAY, + GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_container_add (GTK_CONTAINER (bbox), image); + image = gtk_image_new_from_stock (GTK_STOCK_MEDIA_PAUSE, + GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_container_add (GTK_CONTAINER (bbox), image); + gtk_container_add (GTK_CONTAINER (data->play_pause_button), bbox); + gtk_box_pack_start (GTK_BOX (hbox), + data->play_pause_button, FALSE, FALSE, 0); + g_signal_connect (data->play_pause_button, + "toggled", + G_CALLBACK (play_pause_button_toggled_cb), + data); + + + data->previous_button = gtk_button_new (); + image = gtk_image_new_from_stock (GTK_STOCK_MEDIA_PREVIOUS, + GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_container_add (GTK_CONTAINER (data->previous_button), image); + gtk_box_pack_start (GTK_BOX (hbox), + data->previous_button, FALSE, FALSE, 0); + g_signal_connect (data->previous_button, + "clicked", + G_CALLBACK (previous_button_clicked_cb), + data); + + data->next_button = gtk_button_new (); + image = gtk_image_new_from_stock (GTK_STOCK_MEDIA_NEXT, + GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_container_add (GTK_CONTAINER (data->next_button), image); + gtk_box_pack_start (GTK_BOX (hbox), + data->next_button, FALSE, FALSE, 0); + g_signal_connect (data->next_button, + "clicked", + G_CALLBACK (next_button_clicked_cb), + data); + + button = gtk_button_new (); + image = gtk_image_new_from_stock (GTK_STOCK_REMOVE, + GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_container_add (GTK_CONTAINER (button), image); + gtk_box_pack_end (GTK_BOX (hbox), button, FALSE, FALSE, 0); + g_signal_connect (button, + "clicked", + G_CALLBACK (remove_song_button_clicked_cb), + data); + + + button = gtk_button_new (); + image = gtk_image_new_from_stock (GTK_STOCK_ADD, + GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_container_add (GTK_CONTAINER (button), image); + gtk_box_pack_end (GTK_BOX (hbox), button, FALSE, FALSE, 0); + g_signal_connect (button, + "clicked", + G_CALLBACK (add_song_button_clicked_cb), + data); + +#if 0 + button = gtk_button_new (); + image = gtk_image_new_from_stock (GTK_STOCK_OPEN, + GTK_ICON_SIZE_LARGE_TOOLBAR); + gtk_container_add (GTK_CONTAINER (button), image); + gtk_box_pack_end (GTK_BOX (hbox), button, FALSE, FALSE, 0); + g_signal_connect (button, + "clicked", + G_CALLBACK (open_playlist_button_clicked_cb), + data); +#endif + + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_ALWAYS); + gtk_scrolled_window_set_shadow_type + (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_SHADOW_IN); + gtk_box_pack_start (GTK_BOX (vbox), scrolled_window, TRUE, TRUE, 0); + + data->tree_view = gtk_tree_view_new (); + gtk_container_add (GTK_CONTAINER (scrolled_window), data->tree_view); + + g_signal_connect (data->tree_view, + "row-activated", + G_CALLBACK (row_activated_cb), + data); + + gtk_tree_selection_set_mode + (gtk_tree_view_get_selection (GTK_TREE_VIEW (data->tree_view)), + GTK_SELECTION_MULTIPLE); + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (data->tree_view), + FALSE); + gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (data->tree_view), + TRUE); + gtk_tree_view_set_reorderable (GTK_TREE_VIEW (data->tree_view), + TRUE); + gtk_tree_view_set_rubber_banding (GTK_TREE_VIEW (data->tree_view), + TRUE); + + /** + * Set up list store. + **/ + data->list_store = gtk_list_store_new (3, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_STRING); + + gtk_tree_view_set_model (GTK_TREE_VIEW (data->tree_view), + GTK_TREE_MODEL (data->list_store)); + + gtk_tree_view_insert_column_with_data_func + (GTK_TREE_VIEW (data->tree_view), + -1, "Playing", + gtk_cell_renderer_pixbuf_new (), + (GtkTreeCellDataFunc) playing_cell_func, + data, NULL); + gtk_tree_view_insert_column_with_data_func + (GTK_TREE_VIEW (data->tree_view), + -1, "Song", + gtk_cell_renderer_text_new (), + text_cell_func, + NULL, NULL); + + /** + * Nothing is playing yet. + **/ + set_playing_row (data, NULL); + + /** + * Show it all. + **/ + gtk_widget_show_all (data->window); + + /** + * Enter main loop. + **/ + gtk_main (); + + /** + * Cleanup. + **/ + g_object_unref (data->tag_reader); + g_object_unref (data->playlist_parser); + g_object_unref (data->audio_player); + + if (data->playing_row) + gtk_tree_row_reference_free (data->playing_row); + + gtk_widget_destroy (data->window); + + g_slice_free (AppData, data); + + return 0; +} diff --git a/marshal.list b/marshal.list new file mode 100644 index 0000000..de1dc0b --- /dev/null +++ b/marshal.list @@ -0,0 +1 @@ +VOID:STRING,POINTER,POINTER diff --git a/mockup.py b/mockup.py new file mode 100644 index 0000000..28062c6 --- /dev/null +++ b/mockup.py @@ -0,0 +1,70 @@ +#! /usr/bin/python + +import gobject, gtk + +window = gtk.Window() +window.set_title("Music Player") +window.set_border_width (4) +box = gtk.VBox(False, 4) +window.add(box) + +# Buttons +button_box = gtk.HBox (False, 4) +box.pack_start (button_box, expand=False, fill=False) + +button = gtk.ToggleButton() +b = gtk.HBox(False, 0) +b.add(gtk.image_new_from_stock (gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON)) +b.add(gtk.image_new_from_stock (gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_BUTTON)) +button.add(b) +button_box.pack_start (button, expand=False, fill=False) + +button = gtk.Button() +button.add (gtk.image_new_from_stock (gtk.STOCK_MEDIA_PREVIOUS, gtk.ICON_SIZE_BUTTON)) +button_box.pack_start (button, expand=False, fill=False) + +button = gtk.Button() +button.add (gtk.image_new_from_stock (gtk.STOCK_MEDIA_NEXT, gtk.ICON_SIZE_BUTTON)) +button_box.pack_start (button, expand=False, fill=False) + +button = gtk.Button(label="Add Songs") +button.set_image (gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON)) +button_box.pack_end (button, expand=False, fill=False) + +# Current song +song_box = gtk.HBox (False, 4) +image = gtk.image_new_from_stock(gtk.STOCK_MISSING_IMAGE, gtk.ICON_SIZE_DIALOG) +song_box.pack_start (image, False, False) +l = gtk.Label() +l.set_markup("<big><b>Dreamy Days</b></big>\nRoots Manuva") +l.set_alignment (0.0, 0.0) +song_box.pack_start (l) +box.pack_start(song_box, expand=False, fill=False) + +# Playlist +store = gtk.ListStore(gtk.gdk.Pixbuf, gobject.TYPE_STRING) +store.insert (0, (window.render_icon (gtk.STOCK_MEDIA_PLAY,gtk.ICON_SIZE_MENU), "<b>Dreamy Days</b>\nRoots Manuva")) +store.insert (1, (None, "Hadjaha")) + +view = gtk.TreeView (store) +view.get_selection().set_mode(gtk.SELECTION_MULTIPLE) +view.set_headers_visible(False) +view.set_reorderable (True) +view.set_rubber_banding(True) + +column = gtk.TreeViewColumn() +renderer = gtk.CellRendererPixbuf() +column.pack_start (renderer, False) +column.set_attributes (renderer, pixbuf=0) +renderer = gtk.CellRendererText() +column.pack_start (renderer, True) +column.set_attributes (renderer, markup=1) +view.append_column(column) + +scrolled = gtk.ScrolledWindow() +scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) +scrolled.add(view) +box.pack_start(scrolled, expand=True, fill=True) + +window.show_all() +gtk.main() diff --git a/playlist-parser.c b/playlist-parser.c new file mode 100644 index 0000000..e964136 --- /dev/null +++ b/playlist-parser.c @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2006 OpenedHand Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: Jorn Baayen <jorn@openedhand.com> + */ + +#include <gst/gst.h> +#include <string.h> + +#include "playlist-parser.h" + +G_DEFINE_TYPE (PlaylistParser, + playlist_parser, + G_TYPE_OBJECT); + +enum { + SIGNAL_PLAYLIST_START, + SIGNAL_PLAYLIST_END, + SIGNAL_ENTRY, + SIGNAL_LAST +}; + +static guint signals[SIGNAL_LAST]; + +static void +playlist_parser_init (PlaylistParser *parser) +{ +} + +static void +playlist_parser_dispose (GObject *object) +{ + PlaylistParser *parser; + GObjectClass *object_class; + + parser = PLAYLIST_PARSER (object); + + object_class = G_OBJECT_CLASS (playlist_parser_parent_class); + object_class->dispose (object); +} + +static void +playlist_parser_finalize (GObject *object) +{ + PlaylistParser *parser; + GObjectClass *object_class; + + parser = PLAYLIST_PARSER (object); + + object_class = G_OBJECT_CLASS (playlist_parser_parent_class); + object_class->finalize (object); +} + +static void +playlist_parser_class_init (PlaylistParserClass *klass) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = playlist_parser_dispose; + object_class->finalize = playlist_parser_finalize; + + signals[SIGNAL_PLAYLIST_START] = + g_signal_new ("playlist-start", + TYPE_PLAYLIST_PARSER, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (PlaylistParserClass, + playlist_start), + NULL, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + signals[SIGNAL_PLAYLIST_END] = + g_signal_new ("playlist-end", + TYPE_PLAYLIST_PARSER, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (PlaylistParserClass, + playlist_end), + NULL, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + signals[SIGNAL_ENTRY] = + g_signal_new ("entry", + TYPE_PLAYLIST_PARSER, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (PlaylistParserClass, + entry), + NULL, + NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, + 1, + G_TYPE_STRING); +} + +/** + * playlist_parser_new + * + * Return value: A new #PlaylistParser. + **/ +PlaylistParser * +playlist_parser_new (void) +{ + return g_object_new (TYPE_PLAYLIST_PARSER, NULL); +} + +/** + * Emit the 'entry' signal for @path after converting it to an URI. + **/ +static void +got_absolute_path (PlaylistParser *parser, + const char *path) +{ + char *uri; + + uri = g_filename_to_uri (path, NULL, NULL); + if (!uri) + return; + + g_signal_emit (parser, signals[SIGNAL_ENTRY], 0, uri); + + g_free (uri); +} + +/** + * Parse @channel contents as M3U. + **/ +static void +parse_m3u (PlaylistParser *parser, + GIOChannel *channel, + const char *dirname) +{ + char *line, *p; + gsize length; + + /** + * Signal start of playlist. + **/ + g_signal_emit (parser, signals[SIGNAL_PLAYLIST_START], 0); + + /** + * Parse @channel line by line. + **/ + while (g_io_channel_read_line (channel, + &line, + &length, + NULL, + NULL) == G_IO_STATUS_NORMAL) { + if (line[0] == '#') { + /** + * Ignore comments. + **/ + g_free (line); + + continue; + } + + /** + * This is a normal line. First we de-DOS... + **/ + for (p = line; *p != '\0'; p++) { + switch (*p) { + case '\\': + *p = '/'; + break; + case '\r': + *p = '\0'; + break; + case '\n': + *p = '\0'; + break; + default: + break; + } + } + + /** + * Now we process it. + **/ + if (strstr (line, "://")) { + /** + * This already is an URI. + **/ + g_signal_emit (parser, signals[SIGNAL_ENTRY], 0, line); + } else if (g_path_is_absolute (line)) { + /** + * This is an absolute path. + **/ + got_absolute_path (parser, line); + } else { + char *absolute; + + /** + * This is a relative path. + **/ + absolute = g_build_filename (dirname, line, NULL); + got_absolute_path (parser, absolute); + g_free (absolute); + } + + g_free (line); + } + + /** + * Signal end of playlist. + **/ + g_signal_emit (parser, signals[SIGNAL_PLAYLIST_END], 0); +} + +/** + * playlist_parser_scan_uri + * @parser: A #PlaylistParser + * @uri: An URI + * @error: Location where to store a #GError if an error occurs. + * + * Parse @uri. + * + * Return value: TRUE on success, FALSE if an error occured in which case + * @error is set as well. + **/ +gboolean +playlist_parser_parse (PlaylistParser *parser, + const char *uri, + GError **error) +{ + char *ext, *filename, *dirname; + GIOChannel *channel; + + g_return_val_if_fail (IS_PLAYLIST_PARSER (parser), FALSE); + g_return_val_if_fail (uri != NULL, FALSE); + + /** + * Does @uri point to a M3U file? + **/ + ext = strrchr (uri, '.'); + if (!ext || g_ascii_strcasecmp (ext, ".m3u")) { + g_set_error (error, + PLAYLIST_PARSER_ERROR, + PLAYLIST_PARSER_ERROR_UNKNOWN_TYPE, + "Unknown type"); + + return FALSE; + } + + /** + * Does @uri point to a local file? + **/ + if (g_ascii_strncasecmp (uri, "file:", 5)) { + g_set_error (error, + PLAYLIST_PARSER_ERROR, + PLAYLIST_PARSER_ERROR_UNSUPPORTED_SCHEME, + "Unsupported scheme in URI '%s'", + uri); + + return FALSE; + } + + /** + * Convert @uri to a filename. + **/ + filename = g_filename_from_uri (uri, NULL, error); + if (!filename) + return FALSE; + + /** + * Open @filename for reading. + **/ + channel = g_io_channel_new_file (filename, "r", error); + if (!channel) { + g_free (filename); + + return FALSE; + } + + /** + * Pass channel to parser. + **/ + dirname = g_path_get_dirname (filename); + parse_m3u (parser, channel, dirname); + g_free (dirname); + + /** + * Cleanup. + **/ + g_io_channel_unref (channel); + + g_free (filename); + + return TRUE; +} + +/** + * Returns the playlist parser error quark. + **/ +GQuark +playlist_parser_error_quark (void) +{ + return g_quark_from_static_string ("playlist-parser-error"); +} diff --git a/playlist-parser.h b/playlist-parser.h new file mode 100644 index 0000000..8b93117 --- /dev/null +++ b/playlist-parser.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2006 OpenedHand Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: Jorn Baayen <jorn@openedhand.com> + */ + +#ifndef __PLAYLIST_PARSER_H__ +#define __PLAYLIST_PARSER_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef enum { + PLAYLIST_PARSER_ERROR_UNKNOWN_TYPE, + PLAYLIST_PARSER_ERROR_UNSUPPORTED_SCHEME +} PlaylistParserError; + +GQuark playlist_parser_error_quark (void); + +#define PLAYLIST_PARSER_ERROR \ + (playlist_parser_error_quark ()) + +#define TYPE_PLAYLIST_PARSER \ + (playlist_parser_get_type ()) +#define PLAYLIST_PARSER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), \ + TYPE_PLAYLIST_PARSER, \ + PlaylistParser)) +#define PLAYLIST_PARSER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), \ + TYPE_PLAYLIST_PARSER, \ + PlaylistParserClass)) +#define IS_PLAYLIST_PARSER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \ + TYPE_PLAYLIST_PARSER)) +#define IS_PLAYLIST_PARSER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), \ + TYPE_PLAYLIST_PARSER)) +#define PLAYLIST_PARSER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), \ + TYPE_PLAYLIST_PARSER, \ + PlaylistParserClass)) + +typedef struct { + GObject parent; + + gpointer _reserved; +} PlaylistParser; + +typedef struct { + GObjectClass parent_class; + + /* Signals */ + void (* playlist_start) (PlaylistParser *parser); + void (* playlist_end) (PlaylistParser *parser); + void (* entry) (PlaylistParser *parser, + const char *uri); + + /* Future padding */ + void (* _reserved1) (void); + void (* _reserved2) (void); + void (* _reserved3) (void); + void (* _reserved4) (void); +} PlaylistParserClass; + +GType +playlist_parser_get_type (void) G_GNUC_CONST; + +PlaylistParser * +playlist_parser_new (void); + +gboolean +playlist_parser_parse (PlaylistParser *parser, + const char *uri, + GError **error); + +G_END_DECLS + +#endif /* __PLAYLIST_PARSER_H__ */ diff --git a/tag-reader.c b/tag-reader.c new file mode 100644 index 0000000..ee6a4da --- /dev/null +++ b/tag-reader.c @@ -0,0 +1,649 @@ +/* + * Copyright (C) 2006 OpenedHand Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: Jorn Baayen <jorn@openedhand.com> + */ + +#include <gst/gst.h> +#include <string.h> + +#include "marshal.h" + +#include "tag-reader.h" + +G_DEFINE_TYPE (TagReader, + tag_reader, + G_TYPE_OBJECT); + +struct _TagReaderPrivate { + GstElement *pipeline; + GstElement *src; + GstElement *decodebin; + GstElement *sink; + + GQueue *queue; + + guint next_id; + + GError *current_error; + GstTagList *current_tag_list; +}; + +enum { + SIGNAL_URI_SCANNED, + SIGNAL_LAST +}; + +static guint signals[SIGNAL_LAST]; + +typedef struct { + char *uri; + guint id; +} ScanUriData; + +static void +scan_uri_data_free (ScanUriData *data) +{ + g_free (data->uri); + + g_slice_free (ScanUriData, data); +} + +/** + * Feed the head of the queue to the pipeline. + **/ +static void +feed_head (TagReader *tag_reader) +{ + ScanUriData *data; + + data = g_queue_peek_head (tag_reader->priv->queue); + if (!data) + return; + + /** + * Get appropriate src element. + **/ + tag_reader->priv->src = + gst_element_make_from_uri (GST_URI_SRC, data->uri, "src"); + + /** + * Add to pipeline & link up. + **/ + gst_bin_add (GST_BIN (tag_reader->priv->pipeline), + tag_reader->priv->src); + gst_element_link (tag_reader->priv->src, tag_reader->priv->decodebin); + + /** + * Play pipeline. + **/ + gst_element_set_state (tag_reader->priv->pipeline, + GST_STATE_PLAYING); +} + +/** + * Purge the head of the queue. + **/ +static void +flush_head (TagReader *tag_reader) +{ + ScanUriData *data; + + /** + * Stop pipeline. + **/ + gst_element_set_state (tag_reader->priv->pipeline, + GST_STATE_NULL); + + /** + * Remove source element. + **/ + gst_element_unlink (tag_reader->priv->src, tag_reader->priv->decodebin); + gst_bin_remove (GST_BIN (tag_reader->priv->pipeline), + tag_reader->priv->src); + + /** + * Pop head from queue. + **/ + data = g_queue_pop_head (tag_reader->priv->queue); + + /** + * Call callback. + **/ + g_signal_emit (tag_reader, + signals[SIGNAL_URI_SCANNED], + 0, + data->uri, + tag_reader->priv->current_error, + tag_reader->priv->current_tag_list); + + /** + * Free data. + **/ + if (tag_reader->priv->current_error) { + g_error_free (tag_reader->priv->current_error); + tag_reader->priv->current_error = NULL; + } + + if (tag_reader->priv->current_tag_list) { + gst_tag_list_free (tag_reader->priv->current_tag_list); + tag_reader->priv->current_tag_list = NULL; + } + + scan_uri_data_free (data); +} + +/** + * An error occured. + **/ +static void +bus_message_error_cb (GstBus *bus, + GstMessage *message, + TagReader *tag_reader) +{ + ScanUriData *data; + + data = g_queue_peek_head (tag_reader->priv->queue); + + gst_message_parse_error (message, + &tag_reader->priv->current_error, + NULL); + + flush_head (tag_reader); + feed_head (tag_reader); +} + +/** + * End of stream reached. + **/ +static void +bus_message_eos_cb (GstBus *bus, + GstMessage *message, + TagReader *tag_reader) +{ + flush_head (tag_reader); + feed_head (tag_reader); +} + +/** + * Tag list available. + **/ +static void +bus_message_tag_cb (GstBus *bus, + GstMessage *message, + TagReader *tag_reader) +{ + GstTagList *tags, *new_tags; + + gst_message_parse_tag (message, &tags); + + new_tags = gst_tag_list_merge (tag_reader->priv->current_tag_list, tags, GST_TAG_MERGE_REPLACE); + + if (tag_reader->priv->current_tag_list) + gst_tag_list_free (tag_reader->priv->current_tag_list); + gst_tag_list_free (tags); + + tag_reader->priv->current_tag_list = new_tags; +} + +/** + * Application message received. + **/ +static void +bus_message_application_cb (GstBus *bus, + GstMessage *message, + TagReader *tag_reader) +{ + gpointer src; + const GstStructure *structure; + const char *structure_name; + ScanUriData *data; + GstQuery *query; + + /** + * Verify this is the fakesink handoff event. + **/ + src = GST_MESSAGE_SRC (message); + if (src != tag_reader->priv->sink) + return; + + structure = gst_message_get_structure (message); + structure_name = gst_structure_get_name (structure); + if (strcmp (structure_name, "handoff")) + return; + + /** + * Get relevant ScanUriData. + **/ + data = g_queue_peek_head (tag_reader->priv->queue); + + /** + * Determine the duration. + **/ + query = gst_query_new_duration (GST_FORMAT_TIME); + + if (gst_element_query (tag_reader->priv->pipeline, query)) { + gint64 duration; + + gst_query_parse_duration (query, + NULL, + &duration); + + /** + * Create tag list if none exists yet. + **/ + if (!tag_reader->priv->current_tag_list) { + tag_reader->priv->current_tag_list = + gst_tag_list_new (); + } + + /** + * Merge duration info into tag list. + **/ + gst_tag_list_add (tag_reader->priv->current_tag_list, + GST_TAG_MERGE_REPLACE, + GST_TAG_DURATION, + duration, + NULL); + } + + gst_query_unref (query); + + /** + * Next, please. + **/ + flush_head (tag_reader); + feed_head (tag_reader); +} + +/** + * New decoded pad: Hook up to fakesink. + **/ +static void +decodebin_new_decoded_pad_cb (GstElement *decodebin, + GstPad *pad, + gboolean last, + TagReader *tag_reader) +{ + GstPad *sink_pad; + + /** + * The last discovered pad will always be the one hooked up to + * the sink. + **/ + sink_pad = gst_element_get_pad (tag_reader->priv->sink, "sink"); + gst_pad_link (pad, sink_pad); +} + +/** + * Data of an unknown type fed. + **/ +static void +decodebin_unknown_type_cb (GstElement *decodebin, + GstPad *pad, + GstCaps *caps, + TagReader *tag_reader) +{ + tag_reader->priv->current_error = + g_error_new (TAG_READER_ERROR, + TAG_READER_ERROR_UNKNOWN_TYPE, + "Unknown type"); + + flush_head (tag_reader); + feed_head (tag_reader); +} + +/** + * Fakesink hands over a buffer. + **/ +static void +fakesink_handoff_cb (GstElement *fakesink, + GstBuffer *buffer, + GstPad *pad, + TagReader *tag_reader) +{ + GstStructure *structure; + GstMessage *message; + GstBus *bus; + + /** + * Post a message to the bus, as we are in another thread here. + **/ + structure = gst_structure_new ("handoff", NULL); + message = gst_message_new_application (GST_OBJECT (fakesink), + structure); + + bus = gst_pipeline_get_bus (GST_PIPELINE (tag_reader->priv->pipeline)); + gst_bus_post (bus, message); + gst_object_unref (GST_OBJECT (bus)); +} + +/** + * Constructs the GStreamer pipeline. + **/ +static void +construct_pipeline (TagReader *tag_reader) +{ + + GstBus *bus; + + /** + * The pipeline. + **/ + tag_reader->priv->pipeline = gst_pipeline_new ("pipeline"); + + /** + * No src element yet. + **/ + tag_reader->priv->src = NULL; + + /** + * A decodebin. + **/ + tag_reader->priv->decodebin = + gst_element_factory_make ("decodebin", "decodebin"); + if (!tag_reader->priv->decodebin) { + g_warning ("No decodebin found. Tag reading will not work."); + + return; + } + + gst_bin_add (GST_BIN (tag_reader->priv->pipeline), + tag_reader->priv->decodebin); + + g_signal_connect_object (tag_reader->priv->decodebin, + "new-decoded-pad", + G_CALLBACK (decodebin_new_decoded_pad_cb), + tag_reader, + 0); + g_signal_connect_object (tag_reader->priv->decodebin, + "unknown-type", + G_CALLBACK (decodebin_unknown_type_cb), + tag_reader, + 0); + + /** + * A fakesink. + **/ + tag_reader->priv->sink = + gst_element_factory_make ("fakesink", "fakesink"); + if (!tag_reader->priv->sink) { + g_warning ("No fakesink found. Tag reading will not work."); + + return; + } + + gst_bin_add (GST_BIN (tag_reader->priv->pipeline), + tag_reader->priv->sink); + + g_object_set (tag_reader->priv->sink, + "signal-handoffs", TRUE, + NULL); + + g_signal_connect_object (tag_reader->priv->sink, + "handoff", + G_CALLBACK (fakesink_handoff_cb), + tag_reader, + 0); + + /** + * Connect to signals on bus. + **/ + bus = gst_pipeline_get_bus (GST_PIPELINE (tag_reader->priv->pipeline)); + + gst_bus_add_signal_watch (bus); + + g_signal_connect_object (bus, + "message::error", + G_CALLBACK (bus_message_error_cb), + tag_reader, + 0); + g_signal_connect_object (bus, + "message::eos", + G_CALLBACK (bus_message_eos_cb), + tag_reader, + 0); + g_signal_connect_object (bus, + "message::tag", + G_CALLBACK (bus_message_tag_cb), + tag_reader, + 0); + g_signal_connect_object (bus, + "message::application", + G_CALLBACK (bus_message_application_cb), + tag_reader, + 0); + + gst_object_unref (GST_OBJECT (bus)); +} + +static void +tag_reader_init (TagReader *tag_reader) +{ + /** + * Create pointer to private data. + **/ + tag_reader->priv = + G_TYPE_INSTANCE_GET_PRIVATE (tag_reader, + TYPE_TAG_READER, + TagReaderPrivate); + + /** + * Create scanning queue. + **/ + tag_reader->priv->queue = g_queue_new (); + + /** + * Initialize next ScanUriData ID. + **/ + tag_reader->priv->next_id = 1; + + /** + * No current URI yet. + **/ + tag_reader->priv->current_error = NULL; + tag_reader->priv->current_tag_list = NULL; + + /** + * Construct GStreamer pipeline. + **/ + construct_pipeline (tag_reader); +} + +static void +tag_reader_dispose (GObject *object) +{ + TagReader *tag_reader; + GObjectClass *object_class; + + tag_reader = TAG_READER (object); + + if (tag_reader->priv->pipeline) { + gst_element_set_state (tag_reader->priv->pipeline, + GST_STATE_NULL); + + gst_object_unref (GST_OBJECT (tag_reader->priv->pipeline)); + tag_reader->priv->pipeline = NULL; + } + + object_class = G_OBJECT_CLASS (tag_reader_parent_class); + object_class->dispose (object); +} + +static void +tag_reader_finalize (GObject *object) +{ + TagReader *tag_reader; + GObjectClass *object_class; + + tag_reader = TAG_READER (object); + + if (tag_reader->priv->current_error) + g_error_free (tag_reader->priv->current_error); + if (tag_reader->priv->current_tag_list) + gst_tag_list_free (tag_reader->priv->current_tag_list); + + g_queue_foreach (tag_reader->priv->queue, + (GFunc) scan_uri_data_free, + NULL); + g_queue_free (tag_reader->priv->queue); + + object_class = G_OBJECT_CLASS (tag_reader_parent_class); + object_class->finalize (object); +} + +static void +tag_reader_class_init (TagReaderClass *klass) +{ + GObjectClass *object_class; + + object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = tag_reader_dispose; + object_class->finalize = tag_reader_finalize; + + g_type_class_add_private (klass, sizeof (TagReaderPrivate)); + + signals[SIGNAL_URI_SCANNED] = + g_signal_new ("uri-scanned", + TYPE_TAG_READER, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (TagReaderClass, uri_scanned), + NULL, + NULL, + marshal_VOID__STRING_POINTER_POINTER, + G_TYPE_NONE, + 3, + G_TYPE_STRING, + G_TYPE_POINTER, + G_TYPE_POINTER); +} + +/** + * tag_reader_new + * + * Return value: A new #TagReader. + **/ +TagReader * +tag_reader_new (void) +{ + return g_object_new (TYPE_TAG_READER, NULL); +} + +/** + * tag_reader_scan_uri + * @tag_reader: A #TagReader + * @uri: An URI + * + * Queues @uri up for tag reading. + * + * Return value: A scan ID as @guint. + **/ +guint +tag_reader_scan_uri (TagReader *tag_reader, + const char *uri) +{ + ScanUriData *data; + + g_return_val_if_fail (IS_TAG_READER (tag_reader), 0); + g_return_val_if_fail (uri != NULL, 0); + + data = g_slice_new (ScanUriData); + + data->uri = g_strdup (uri); + data->id = tag_reader->priv->next_id++; + + g_queue_push_tail (tag_reader->priv->queue, data); + + if (g_queue_get_length (tag_reader->priv->queue) == 1) { + /** + * The queue was empty, so we were idle. This means + * we need to start the pump again by feeding the new + * uri to the pipeline. + **/ + feed_head (tag_reader); + } + + return data->id; +} + +/** + * Find a ScanUriData by its ID. + **/ +static int +find_scan_uri_data (gconstpointer a, + gconstpointer b) +{ + guint ai = GPOINTER_TO_UINT (a); + guint bi = GPOINTER_TO_UINT (b); + + if (ai < bi) + return -1; + else if (ai > bi) + return 1; + else + return 0; +} + +/** + * tag_reader_cancal_scan_uri + * @tag_reader: A #TagReader + * @scan_id: The #guint scan ID as returned by #tag_reader_scan_uri + * + * Cancels the scanning of URI with ID @scan_id. + **/ +void +tag_reader_cancel_scan_uri (TagReader *tag_reader, + guint scan_id) +{ + GList *link; + + g_return_if_fail (IS_TAG_READER (tag_reader)); + + link = g_queue_find_custom (tag_reader->priv->queue, + GUINT_TO_POINTER (scan_id), + find_scan_uri_data); + if (!link) { + g_warning ("Not scanning URI with ID %u", scan_id); + + return; + } + + if (!link->prev) { + /** + * We were just processing this one. Use standard + * flushing process. + **/ + flush_head (tag_reader); + } else { + /** + * This one is queued up. Dequeue. + **/ + scan_uri_data_free (link->data); + g_queue_delete_link (tag_reader->priv->queue, link); + } +} + +/** + * Returns the tag reader error quark. + **/ +GQuark +tag_reader_error_quark (void) +{ + return g_quark_from_static_string ("tag-reader-error"); +} diff --git a/tag-reader.h b/tag-reader.h new file mode 100644 index 0000000..0086258 --- /dev/null +++ b/tag-reader.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2006 OpenedHand Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: Jorn Baayen <jorn@openedhand.com> + */ + +#ifndef __TAG_READER_H__ +#define __TAG_READER_H__ + +#include <glib-object.h> +#include <gst/gsttaglist.h> + +G_BEGIN_DECLS + +typedef enum { + TAG_READER_ERROR_UNKNOWN_TYPE +} TagReaderError; + +GQuark tag_reader_error_quark (void); + +#define TAG_READER_ERROR \ + (tag_reader_error_quark ()) + +#define TYPE_TAG_READER \ + (tag_reader_get_type ()) +#define TAG_READER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), \ + TYPE_TAG_READER, \ + TagReader)) +#define TAG_READER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), \ + TYPE_TAG_READER, \ + TagReaderClass)) +#define IS_TAG_READER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \ + TYPE_TAG_READER)) +#define IS_TAG_READER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), \ + TYPE_TAG_READER)) +#define TAG_READER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), \ + TYPE_TAG_READER, \ + TagReaderClass)) + +typedef struct _TagReaderPrivate TagReaderPrivate; + +typedef struct { + GObject parent; + + TagReaderPrivate *priv; +} TagReader; + +typedef struct { + GObjectClass parent_class; + + /* Signals */ + void (* uri_scanned) (TagReader *tag_reader, + const char *uri, + GError *error, + GstTagList *tag_list); + + /* Future padding */ + void (* _reserved1) (void); + void (* _reserved2) (void); + void (* _reserved3) (void); + void (* _reserved4) (void); +} TagReaderClass; + +GType +tag_reader_get_type (void) G_GNUC_CONST; + +TagReader * +tag_reader_new (void); + +guint +tag_reader_scan_uri (TagReader *tag_reader, + const char *uri); + +void +tag_reader_cancel_scan_uri (TagReader *tag_reader, + guint scan_id); + +G_END_DECLS + +#endif /* __TAG_READER_H__ */ |