2 * Copyright (C) 2009 Jens Georg <mail@jensge.org>.
4 * This file is part of Rygel.
6 * Rygel is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * Rygel is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
25 internal class FileQueueEntry {
28 public string content_type;
30 public FileQueueEntry (File file, bool known, string content_type) {
33 this.content_type = content_type;
37 public class Rygel.MediaExport.HarvestingTask : Rygel.StateMachine,
40 private MetadataExtractor extractor;
41 private MediaCache cache;
42 private GLib.Queue<MediaContainer> containers;
43 private Gee.Queue<FileQueueEntry> files;
44 private RecursiveFileMonitor monitor;
45 private MediaContainer parent;
46 private const int BATCH_SIZE = 256;
48 public Cancellable cancellable { get; set; }
50 private const string HARVESTER_ATTRIBUTES =
51 FileAttribute.STANDARD_NAME + "," +
52 FileAttribute.STANDARD_TYPE + "," +
53 FileAttribute.TIME_MODIFIED + "," +
54 FileAttribute.STANDARD_CONTENT_TYPE + "," +
55 FileAttribute.STANDARD_SIZE + "," +
56 FileAttribute.STANDARD_IS_HIDDEN;
58 public HarvestingTask (RecursiveFileMonitor monitor,
60 MediaContainer parent) {
61 this.extractor = new MetadataExtractor ();
64 this.cache = MediaCache.get_default ();
66 this.extractor.extraction_done.connect (on_extracted_cb);
67 this.extractor.error.connect (on_extractor_error_cb);
69 this.files = new LinkedList<FileQueueEntry> ();
70 this.containers = new GLib.Queue<MediaContainer> ();
71 this.monitor = monitor;
74 public void cancel () {
75 // detach from common cancellable; otherwise everything would be
76 // cancelled like file monitoring, other harvesters etc.
77 this.cancellable = new Cancellable ();
78 this.cancellable.cancel ();
82 * Extract all metainformation from a given file.
84 * What action will be taken depends on the arguments
85 * * file is a simple file. Then only information of this
86 * file will be extracted
87 * * file is a directory and recursive is false. The children
88 * of the directory (if not directories themselves) will be
89 * enqueued for extraction
90 * * file is a directory and recursive is true. ++ All ++ children
91 * of the directory will be enqueued for extraction, even directories
93 * No matter how many children are contained within file's hierarchy,
94 * only one event is sent when all the children are done.
96 public async void run () {
98 var info = yield this.origin.query_info_async
99 (HARVESTER_ATTRIBUTES,
100 FileQueryInfoFlags.NONE,
104 if (this.process_file (this.origin, info, this.parent)) {
105 if (info.get_file_type () != FileType.DIRECTORY) {
106 this.containers.push_tail (this.parent);
112 } catch (Error error) {
113 if (!(error is IOError.CANCELLED)) {
114 warning (_("Failed to harvest file %s: %s"),
115 this.origin.get_uri (),
118 debug ("Harvesting of uri %s was cancelled",
119 this.origin.get_uri ());
126 * Add a file to the meta-data extraction queue.
128 * The file will only be added to the queue if one of the following
130 * - The file is not in the cache
131 * - The current mtime of the file is larger than the cached
132 * - The size has changed
133 * @param file to check
134 * @param info FileInfo of the file to check
135 * @return true, if the file has been queued, false otherwise.
137 private bool push_if_changed_or_unknown (File file,
142 if (this.cache.exists (file, out timestamp, out size)) {
143 int64 mtime = (int64) info.get_attribute_uint64
144 (FileAttribute.TIME_MODIFIED);
146 if (mtime > timestamp ||
147 info.get_size () != size) {
148 var entry = new FileQueueEntry (file,
150 info.get_content_type ());
151 this.files.offer (entry);
156 var entry = new FileQueueEntry (file,
158 info.get_content_type ());
159 this.files.offer (entry);
163 } catch (Error error) {
164 warning (_("Failed to query database: %s"), error.message);
170 private bool process_file (File file,
172 MediaContainer parent) {
173 if (info.get_is_hidden ()) {
177 if (info.get_file_type () == FileType.DIRECTORY) {
178 // queue directory for processing later
179 this.monitor.add.begin (file);
181 var container = new DummyContainer (file, parent);
182 this.containers.push_tail (container);
184 // Only add new containers. There's not much about a container so
185 // we skip the updated signal
186 var dummy_parent = parent as DummyContainer;
187 if (dummy_parent == null ||
188 !dummy_parent.children.contains (MediaCache.get_id (file))) {
189 (parent as TrackableContainer).add_child_tracked.begin (container);
194 // Check if the file needs to be harvested at all either because
195 // it is denied by filter or it hasn't updated
196 if (Harvester.is_eligible (info)) {
197 return this.push_if_changed_or_unknown (file, info);
204 private bool process_children (GLib.List<FileInfo>? list) {
205 if (list == null || this.cancellable.is_cancelled ()) {
209 var container = this.containers.peek_head () as DummyContainer;
211 foreach (var info in list) {
212 var file = container.file.get_child (info.get_name ());
214 this.process_file (file, info, container);
215 container.seen (file);
221 private async void enumerate_directory () {
222 var directory = (this.containers.peek_head () as DummyContainer).file;
224 var enumerator = yield directory.enumerate_children_async
225 (HARVESTER_ATTRIBUTES,
226 FileQueryInfoFlags.NONE,
230 GLib.List<FileInfo> list = null;
232 list = yield enumerator.next_files_async (BATCH_SIZE,
235 } while (this.process_children (list));
237 yield enumerator.close_async (Priority.DEFAULT, this.cancellable);
238 } catch (Error err) {
239 warning (_("failed to enumerate folder: %s"), err.message);
242 this.cleanup_database ();
246 private void cleanup_database () {
247 var container = this.containers.peek_head () as DummyContainer;
249 // delete all children which are not in filesystem anymore
251 foreach (var child in container.children) {
252 this.cache.remove_by_id (child);
254 } catch (DatabaseError error) {
255 warning (_("Failed to get children of container %s: %s"),
261 private bool on_idle () {
262 if (this.cancellable.is_cancelled ()) {
268 if (!this.files.is_empty) {
269 debug ("Scheduling file %s for meta-data extraction…",
270 this.files.peek ().file.get_uri ());
271 this.extractor.extract (this.files.peek ().file,
272 this.files.peek ().content_type);
273 } else if (!this.containers.is_empty ()) {
274 this.enumerate_directory.begin ();
283 private void on_extracted_cb (File file,
284 DiscovererInfo? dlna,
285 GUPnPDLNA.Profile? profile,
286 FileInfo file_info) {
287 if (this.cancellable.is_cancelled ()) {
293 item = ItemFactory.create_simple (this.containers.peek_head (),
297 item = ItemFactory.create_from_info (this.containers.peek_head (),
305 item.parent_ref = this.containers.peek_head ();
306 // This is only necessary to generate the proper <objAdd LastChange
308 if (this.files.peek ().known) {
309 (item as UpdatableObject).non_overriding_commit.begin ();
311 var container = item.parent as TrackableContainer;
312 container.add_child_tracked.begin (item) ;
320 private void on_extractor_error_cb (File file, Error error) {
321 // error is only emitted if even the basic information extraction
322 // failed; there's not much to do here, just print the information and
323 // go to the next file
325 debug ("Skipping %s; extraction completely failed: %s",
334 * If all files of a container were processed, notify the container
335 * about this and set the updating signal.
336 * Reschedule the iteration and extraction
338 private void do_update () {
339 if (this.files.is_empty &&
340 !this.containers.is_empty ()) {
341 this.containers.pop_head ();