8cbec76b9543e065bc1a1046227d7b63aaac08c0
[profile/ivi/rygel.git] / src / plugins / media-export / rygel-media-export-harvesting-task.vala
1 /*
2  * Copyright (C) 2009 Jens Georg <mail@jensge.org>.
3  *
4  * This file is part of Rygel.
5  *
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.
10  *
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.
15  *
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.
19  */
20
21 using GLib;
22 using Gee;
23 using Gst.PbUtils;
24
25 internal class FileQueueEntry {
26     public File file;
27     public bool known;
28     public string content_type;
29
30     public FileQueueEntry (File file, bool known, string content_type) {
31         this.file = file;
32         this.known = known;
33         this.content_type = content_type;
34     }
35 }
36
37 public class Rygel.MediaExport.HarvestingTask : Rygel.StateMachine,
38                                                 GLib.Object {
39     public File origin;
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;
47
48     public Cancellable cancellable { get; set; }
49
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;
57
58     public HarvestingTask (RecursiveFileMonitor monitor,
59                            File                 file,
60                            MediaContainer       parent) {
61         this.extractor = new MetadataExtractor ();
62         this.origin = file;
63         this.parent = parent;
64         this.cache = MediaCache.get_default ();
65
66         this.extractor.extraction_done.connect (on_extracted_cb);
67         this.extractor.error.connect (on_extractor_error_cb);
68
69         this.files = new LinkedList<FileQueueEntry> ();
70         this.containers = new GLib.Queue<MediaContainer> ();
71         this.monitor = monitor;
72     }
73
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 ();
79     }
80
81     /**
82      * Extract all metainformation from a given file.
83      *
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
92      *
93      * No matter how many children are contained within file's hierarchy,
94      * only one event is sent when all the children are done.
95      */
96     public async void run () {
97         try {
98             var info = yield this.origin.query_info_async
99                                         (HARVESTER_ATTRIBUTES,
100                                          FileQueryInfoFlags.NONE,
101                                          Priority.DEFAULT,
102                                          this.cancellable);
103
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);
107                 }
108                 this.on_idle ();
109             } else {
110                 this.completed ();
111             }
112         } catch (Error error) {
113             if (!(error is IOError.CANCELLED)) {
114                 warning (_("Failed to harvest file %s: %s"),
115                          this.origin.get_uri (),
116                          error.message);
117             } else {
118                 debug ("Harvesting of uri %s was cancelled",
119                        this.origin.get_uri ());
120             }
121             this.completed ();
122         }
123     }
124
125     /**
126      * Add a file to the meta-data extraction queue.
127      *
128      * The file will only be added to the queue if one of the following
129      * conditions is met:
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.
136      */
137     private bool push_if_changed_or_unknown (File       file,
138                                              FileInfo   info) {
139         int64 timestamp;
140         int64 size;
141         try {
142             if (this.cache.exists (file, out timestamp, out size)) {
143                 int64 mtime = (int64) info.get_attribute_uint64
144                                         (FileAttribute.TIME_MODIFIED);
145
146                 if (mtime > timestamp ||
147                     info.get_size () != size) {
148                     var entry = new FileQueueEntry (file,
149                                                     true,
150                                                     info.get_content_type ());
151                     this.files.offer (entry);
152
153                     return true;
154                 }
155             } else {
156                 var entry = new FileQueueEntry (file,
157                                                 false,
158                                                 info.get_content_type ());
159                 this.files.offer (entry);
160
161                 return true;
162             }
163         } catch (Error error) {
164             warning (_("Failed to query database: %s"), error.message);
165         }
166
167         return false;
168     }
169
170     private bool process_file (File           file,
171                                FileInfo       info,
172                                MediaContainer parent) {
173         if (info.get_is_hidden ()) {
174             return false;
175         }
176
177         if (info.get_file_type () == FileType.DIRECTORY) {
178             // queue directory for processing later
179             this.monitor.add.begin (file);
180
181             var container = new DummyContainer (file, parent);
182             this.containers.push_tail (container);
183
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);
190             }
191
192             return true;
193         } else {
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);
198             }
199
200             return false;
201         }
202     }
203
204     private bool process_children (GLib.List<FileInfo>? list) {
205         if (list == null || this.cancellable.is_cancelled ()) {
206             return false;
207         }
208
209         var container = this.containers.peek_head () as DummyContainer;
210
211         foreach (var info in list) {
212             var file = container.file.get_child (info.get_name ());
213
214             this.process_file (file, info, container);
215             container.seen (file);
216         }
217
218         return true;
219     }
220
221     private async void enumerate_directory () {
222         var directory = (this.containers.peek_head () as DummyContainer).file;
223         try {
224             var enumerator = yield directory.enumerate_children_async
225                                         (HARVESTER_ATTRIBUTES,
226                                          FileQueryInfoFlags.NONE,
227                                          Priority.DEFAULT,
228                                          this.cancellable);
229
230             GLib.List<FileInfo> list = null;
231             do {
232                 list = yield enumerator.next_files_async (BATCH_SIZE,
233                                                           Priority.DEFAULT,
234                                                           this.cancellable);
235             } while (this.process_children (list));
236
237             yield enumerator.close_async (Priority.DEFAULT, this.cancellable);
238         } catch (Error err) {
239             warning (_("failed to enumerate folder: %s"), err.message);
240         }
241
242         this.cleanup_database ();
243         this.do_update ();
244     }
245
246     private void cleanup_database () {
247         var container = this.containers.peek_head () as DummyContainer;
248
249         // delete all children which are not in filesystem anymore
250         try {
251             foreach (var child in container.children) {
252                 this.cache.remove_by_id (child);
253             }
254         } catch (DatabaseError error) {
255             warning (_("Failed to get children of container %s: %s"),
256                      container.id,
257                      error.message);
258         }
259     }
260
261     private bool on_idle () {
262         if (this.cancellable.is_cancelled ()) {
263             this.completed ();
264
265             return false;
266         }
267
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 ();
275         } else {
276             // nothing to do
277             this.completed ();
278         }
279
280         return false;
281     }
282
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 ()) {
288             this.completed ();
289         }
290
291         MediaItem item;
292         if (dlna == null) {
293             item = ItemFactory.create_simple (this.containers.peek_head (),
294                                               file,
295                                               file_info);
296         } else {
297             item = ItemFactory.create_from_info (this.containers.peek_head (),
298                                                  file,
299                                                  dlna,
300                                                  profile,
301                                                  file_info);
302         }
303
304         if (item != null) {
305             item.parent_ref = this.containers.peek_head ();
306             // This is only necessary to generate the proper <objAdd LastChange
307             // entry
308             if (this.files.peek ().known) {
309                 (item as UpdatableObject).non_overriding_commit.begin ();
310             } else {
311                 var container = item.parent as TrackableContainer;
312                 container.add_child_tracked.begin (item) ;
313             }
314         }
315
316         this.files.poll ();
317         this.do_update ();
318     }
319
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
324
325         debug ("Skipping %s; extraction completely failed: %s",
326                file.get_uri (),
327                error.message);
328
329         this.files.poll ();
330         this.do_update ();
331     }
332
333     /**
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
337      */
338     private void do_update () {
339         if (this.files.is_empty &&
340             !this.containers.is_empty ()) {
341             this.containers.pop_head ();
342         }
343
344         this.on_idle ();
345     }
346 }