strip added smb:// shares of their user/pass when adding, and instead store that...
[vuplus_xbmc] / xbmc / filesystem / MythDirectory.cpp
1 /*
2  *      Copyright (C) 2005-2008 Team XBMC
3  *      http://www.xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, write to
17  *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
18  *  http://www.gnu.org/copyleft/gpl.html
19  *
20  */
21
22 #include "MythDirectory.h"
23 #include "MythSession.h"
24 #include "utils/URIUtils.h"
25 #include "DllLibCMyth.h"
26 #include "video/VideoInfoTag.h"
27 #include "URL.h"
28 #include "settings/GUISettings.h"
29 #include "settings/AdvancedSettings.h"
30 #include "FileItem.h"
31 #include "utils/StringUtils.h"
32 #include "guilib/LocalizeStrings.h"
33 #include "utils/log.h"
34 #include "DirectoryCache.h"
35 #include "utils/TimeUtils.h"
36
37 extern "C"
38 {
39 #include "cmyth/include/cmyth/cmyth.h"
40 #include "cmyth/include/refmem/refmem.h"
41 }
42
43 using namespace XFILE;
44 using namespace std;
45
46 CMythDirectory::CMythDirectory()
47 {
48   m_session  = NULL;
49   m_dll      = NULL;
50   m_database = NULL;
51   m_recorder = NULL;
52 }
53
54 CMythDirectory::~CMythDirectory()
55 {
56   Release();
57 }
58
59 DIR_CACHE_TYPE CMythDirectory::GetCacheType(const CStdString& strPath) const
60 {
61   CURL url(strPath);
62   CStdString fileName = url.GetFileName();
63   URIUtils::RemoveSlashAtEnd(fileName);
64
65   /*
66    * Always cache "All Recordings", "Guide" (top folder only), "Movies", and "TV Shows" (including
67    * sub folders).
68    *
69    * Entire directory cache for myth:// is invalidated when the root directory is requested to
70    * ensure content is always up-to-date.
71    */
72   if (fileName == "recordings"
73   ||  fileName == "guide"
74   ||  fileName == "movies"
75   ||  fileName.Left(7) == "tvshows")
76     return DIR_CACHE_ALWAYS;
77
78   return DIR_CACHE_ONCE;
79 }
80
81 void CMythDirectory::Release()
82 {
83   if (m_recorder)
84   {
85     m_dll->ref_release(m_recorder);
86     m_recorder = NULL;
87   }
88   if (m_session)
89   {
90     CMythSession::ReleaseSession(m_session);
91     m_session = NULL;
92   }
93   m_dll = NULL;
94 }
95
96 bool CMythDirectory::GetGuide(const CStdString& base, CFileItemList &items)
97 {
98   cmyth_database_t db = m_session->GetDatabase();
99   if (!db)
100     return false;
101
102   cmyth_chanlist_t list = m_dll->mysql_get_chanlist(db);
103   if (!list)
104   {
105     CLog::Log(LOGERROR, "%s - Unable to get list of channels: %s", __FUNCTION__, base.c_str());
106     return false;
107   }
108   CURL url(base);
109
110   int count = m_dll->chanlist_get_count(list);
111   for (int i = 0; i < count; i++)
112   {
113     cmyth_channel_t channel = m_dll->chanlist_get_item(list, i);
114     if (channel)
115     {
116       if (!m_dll->channel_visible(channel))
117       {
118         m_dll->ref_release(channel);
119         continue;
120       }
121
122       int channum = m_dll->channel_channum(channel); // e.g. 3
123       CStdString name = GetValue(m_dll->channel_name(channel)); // e.g. TV3
124       if (channum <= 0)
125       {
126         CLog::Log(LOGDEBUG, "%s - Skipping channel number %d as <= 0: %s", __FUNCTION__, channum, name.c_str());
127         m_dll->ref_release(channel);
128         continue;
129       }
130
131       CLog::Log(LOGDEBUG, "%s - Adding channel number %d: %s", __FUNCTION__, channum, name.c_str());
132
133       CStdString number;
134       number.Format("%d", channum); // CStdString easier for string manipulation than int.
135       url.SetFileName("guide/" + number);
136       CFileItemPtr item(new CFileItem(url.Get(), true));
137       item->m_strTitle = number;
138       if (!name.IsEmpty())
139         item->m_strTitle += " - " + name; // e.g. 3 - TV3
140
141       CStdString icon = GetValue(m_dll->channel_icon(channel));
142       if (!icon.IsEmpty())
143       {
144         url.SetFileName("files/channels/" + URIUtils::GetFileName(icon)); // e.g. files/channels/tv3.jpg
145         item->SetThumbnailImage(url.Get());
146       }
147
148       items.Add(item);
149
150       m_dll->ref_release(channel);
151     }
152   }
153
154   items.AddSortMethod(SORT_METHOD_LABEL, 551 /* Name */, LABEL_MASKS("", "", "%K", ""));
155
156   m_dll->ref_release(list);
157   return true;
158 }
159
160 bool CMythDirectory::GetGuideForChannel(const CStdString& base, CFileItemList &items, int channelNumber)
161 {
162   cmyth_database_t database = m_session->GetDatabase();
163   if (!database)
164   {
165     CLog::Log(LOGERROR, "%s - Could not get database", __FUNCTION__);
166     return false;
167   }
168
169   time_t now;
170   time(&now);
171   time_t end = now + (24 * 60 * 60); // How many seconds of EPG from now we should grab, 24 hours in seconds
172
173   cmyth_program_t *program = NULL;
174   // TODO: See if there is a way to just get the entries for the chosen channel rather than ALL
175   int count = m_dll->mysql_get_guide(database, &program, now, end);
176   CLog::Log(LOGDEBUG, "%s - %i entries in guide data", __FUNCTION__, count);
177   if (count <= 0)
178     return false;
179
180   for (int i = 0; i < count; i++)
181   {
182     if (program[i].channum == channelNumber)
183     {
184       CFileItemPtr item(new CFileItem("", false)); // No path for guide entries
185
186       /*
187        * Set the FileItem meta data.
188        */
189       CStdString title        = program[i].title; // e.g. Mythbusters
190       CStdString subtitle     = program[i].subtitle; // e.g. The Pirate Special
191       CDateTime localstart;
192       if (program[i].starttime)
193         localstart = CTimeUtils::GetLocalTime(program[i].starttime);
194       item->m_strTitle.Format("%s - %s", localstart.GetAsLocalizedTime("HH:mm", false), title); // e.g. 20:30 - Mythbusters
195       if (!subtitle.IsEmpty())
196         item->m_strTitle     += " - \"" + subtitle + "\""; // e.g. 20:30 - Mythbusters - "The Pirate Special"
197       item->m_dateTime        = localstart;
198
199       /*
200        * Set the VideoInfoTag meta data so it matches the FileItem meta data where possible.
201        */
202       CVideoInfoTag* tag      = item->GetVideoInfoTag();
203       tag->m_strTitle         = title;
204       if (!subtitle.IsEmpty())
205         tag->m_strTitle      += " - \"" + subtitle + "\""; // e.g. Mythbusters - "The Pirate Special"
206       tag->m_strShowTitle     = title;
207       tag->m_strOriginalTitle = title;
208       tag->m_strPlotOutline   = subtitle;
209       tag->m_strPlot          = program[i].description;
210       // TODO: Strip out the subtitle from the description if it is present at the start?
211       // TODO: Do we need to add the subtitle to the start of the plot if not already as it used to? Seems strange, should be handled by skin?
212       tag->m_genre            = StringUtils::Split(program[i].category, g_advancedSettings.m_videoItemSeparator); // e.g. Sports
213       tag->m_strAlbum         = program[i].callsign; // e.g. TV3
214
215       CDateTime start(program[i].starttime);
216       CDateTime end(program[i].endtime);
217       CDateTimeSpan runtime = end - start;
218       tag->m_strRuntime       = StringUtils::SecondsToTimeString(runtime.GetSeconds() +
219                                                                  runtime.GetMinutes() * 60 +
220                                                                  runtime.GetHours() * 3600);
221       tag->m_iSeason          = 0; // So XBMC treats the content as an episode and displays tag information.
222       tag->m_iEpisode         = 0;
223
224       items.Add(item);
225     }
226   }
227
228   /*
229    * Items are sorted as added to the list (in ascending date order). Specifying sorting by date can
230    * result in the guide being shown in the wrong order for skins that sort by date in descending
231    * order by default with no option to change to ascending, e.g. Confluence.
232    */
233   items.AddSortMethod(SORT_METHOD_NONE, 552 /* Date */, LABEL_MASKS("%K", "%J")); // Still leave the date label
234
235   m_dll->ref_release(program);
236   return true;
237 }
238
239 bool CMythDirectory::GetRecordings(const CStdString& base, CFileItemList &items, enum FilterType type,
240                                     const CStdString& filter)
241 {
242   cmyth_proglist_t list = m_session->GetAllRecordedPrograms();
243   if (!list)
244   {
245     CLog::Log(LOGERROR, "%s - unable to get list of recordings", __FUNCTION__);
246     return false;
247   }
248
249   int count = m_dll->proglist_get_count(list);
250   for (int i = 0; i < count; i++)
251   {
252     cmyth_proginfo_t program = m_dll->proglist_get_item(list, i);
253     if (program)
254     {
255       if (!IsVisible(program))
256       {
257         m_dll->ref_release(program);
258         continue;
259       }
260
261       CURL url(base);
262       /*
263        * The base is the URL used to connect to the master server. The hostname in this may not
264        * appropriate for all items as MythTV supports multiple backends (master + slaves).
265        *
266        * The appropriate host for playback is contained in the program information sent back from
267        * the master. The same username and password are used in the URL as for the master.
268        */
269       url.SetHostName(GetValue(m_dll->proginfo_host(program)));
270
271       CStdString path = URIUtils::GetFileName(GetValue(m_dll->proginfo_pathname(program)));
272       CStdString name = GetValue(m_dll->proginfo_title(program));
273
274       switch (type)
275       {
276       case MOVIES:
277         if (!IsMovie(program))
278         {
279           m_dll->ref_release(program);
280           continue;
281         }
282         url.SetFileName("movies/" + path);
283         break;
284       case TV_SHOWS:
285         if (filter.CompareNoCase(name))
286         {
287           m_dll->ref_release(program);
288           continue;
289         }
290         url.SetFileName("tvshows/" + name + "/" + path);
291         break;
292       case ALL:
293         url.SetFileName("recordings/" + path);
294         break;
295       }
296
297       CFileItemPtr item(new CFileItem(url.Get(), false));
298       m_session->SetFileItemMetaData(*item, program);
299
300       /*
301        * If MOVIES, set the label and specify as pre-formatted so any scraper lookup will use the
302        * label rather than the filename. Don't set as pre-formatted for any other types as this
303        * prevents the display of the title changing depending on what the list is being sorted by.
304        */
305       if (type == MOVIES)
306       {
307         /*
308          * Adding the production year, if available, to the label for Movies to aid in scraper
309          * lookups.
310          */
311         CStdString label(item->m_strTitle);
312         CStdString prodyear = GetValue(m_dll->proginfo_prodyear(program));
313         if (!prodyear.IsEmpty())
314           label += " (" + prodyear + ")";
315         item->SetLabel(label);
316         item->SetLabelPreformated(true);
317       }
318
319       items.Add(item);
320       m_dll->ref_release(program);
321     }
322   }
323   m_dll->ref_release(list);
324
325   /*
326    * Don't sort by name for TV_SHOWS as they all have the same name, so only date sort is useful.
327    * Since the subtitle has been added to the TV Show name, the video sort title sort is used so
328    * the subtitle doesn't influence the sort order and they are sorted by date.
329    */
330   if (type != TV_SHOWS)
331   {
332     if (g_guiSettings.GetBool("filelists.ignorethewhensorting"))
333       items.AddSortMethod(SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE, 551 /* Name */, LABEL_MASKS("%K", "%J"));
334     else
335       items.AddSortMethod(SORT_METHOD_VIDEO_SORT_TITLE, 551 /* Name */, LABEL_MASKS("%K", "%J"));
336   }
337   items.AddSortMethod(SORT_METHOD_DATE, 552 /* Date */, LABEL_MASKS("%K", "%J"));
338
339   return true;
340 }
341
342 /**
343  * \brief Gets a list of folders for recorded TV shows
344  */
345 bool CMythDirectory::GetTvShowFolders(const CStdString& base, CFileItemList &items)
346 {
347   cmyth_proglist_t list = m_session->GetAllRecordedPrograms();
348   if (!list)
349   {
350     CLog::Log(LOGERROR, "%s - unable to get list of recordings", __FUNCTION__);
351     return false;
352   }
353
354   int count = m_dll->proglist_get_count(list);
355   for (int i = 0; i < count; i++)
356   {
357     cmyth_proginfo_t program = m_dll->proglist_get_item(list, i);
358     if (program)
359     {
360       if (!IsVisible(program))
361       {
362         m_dll->ref_release(program);
363         continue;
364       }
365
366       if (!IsTvShow(program))
367       {
368         m_dll->ref_release(program);
369         continue;
370       }
371
372       CStdString title = GetValue(m_dll->proginfo_title(program));
373       CStdString path = base + "/" + title + "/";
374
375       /*
376        * Only add each TV show once. If the TV show is already in the list, update the date for the
377        * folder to be the date of the last recorded TV show as the programs are returned in the
378        * order they were recorded.
379        */
380       if (items.Contains(path))
381       {
382         CFileItemPtr item = items.Get(path);
383         item->m_dateTime = GetValue(m_dll->proginfo_rec_start(program));
384       }
385       else
386       {
387         CFileItemPtr item(new CFileItem(path, true));
388         item->m_dateTime = GetValue(m_dll->proginfo_rec_start(program));
389         item->SetLabel(title);
390         items.Add(item);
391       }
392       m_dll->ref_release(program);
393     }
394
395   }
396   m_dll->ref_release(list);
397
398   if (g_guiSettings.GetBool("filelists.ignorethewhensorting"))
399     items.AddSortMethod(SORT_METHOD_LABEL_IGNORE_THE, 551 /* Name */, LABEL_MASKS("", "", "%L", "%J"));
400   else
401     items.AddSortMethod(SORT_METHOD_LABEL, 551 /* Name */, LABEL_MASKS("", "", "%L", "%J"));
402   items.AddSortMethod(SORT_METHOD_DATE, 552 /* Date */, LABEL_MASKS("", "", "%L", "%J"));
403
404   return true;
405 }
406
407 bool CMythDirectory::GetChannels(const CStdString& base, CFileItemList &items)
408 {
409   cmyth_conn_t control = m_session->GetControl();
410   if (!control)
411     return false;
412
413   vector<cmyth_proginfo_t> channels;
414   for (unsigned i = 0; i < 16; i++)
415   {
416     cmyth_recorder_t recorder = m_dll->conn_get_recorder_from_num(control, i);
417     if (!recorder)
418       continue;
419
420     cmyth_proginfo_t program;
421     program = m_dll->recorder_get_cur_proginfo(recorder);
422     program = m_dll->recorder_get_next_proginfo(recorder, program, BROWSE_DIRECTION_UP);
423     if (!program)
424     {
425       m_dll->ref_release(m_recorder);
426       continue;
427     }
428
429     long startchan = m_dll->proginfo_chan_id(program);
430     long currchan  = -1;
431     while (startchan != currchan)
432     {
433       unsigned j;
434       for (j = 0; j < channels.size(); j++)
435       {
436         if (m_dll->proginfo_compare(program, channels[j]) == 0)
437           break;
438       }
439
440       if (j == channels.size())
441         channels.push_back(program);
442
443       program = m_dll->recorder_get_next_proginfo(recorder, program, BROWSE_DIRECTION_UP);
444       if (!program)
445         break;
446
447       currchan = m_dll->proginfo_chan_id(program);
448     }
449     m_dll->ref_release(recorder);
450   }
451
452   CURL url(base);
453   /*
454    * The content of the cmyth_proginfo_t struct retrieved and stored in channels[] above does not
455    * contain the host so the URL cannot be modified to support both master and slave servers.
456    */
457
458   for (unsigned i = 0; i < channels.size(); i++)
459   {
460     cmyth_proginfo_t program = channels[i];
461
462     url.SetFileName("channels/" + GetValue(m_dll->proginfo_chanstr(program)) + ".ts"); // e.g. 3.ts
463     CFileItemPtr item(new CFileItem(url.Get(), false));
464     m_session->SetFileItemMetaData(*item, program);
465
466     items.Add(item);
467     m_dll->ref_release(program);
468   }
469
470   items.AddSortMethod(SORT_METHOD_LABEL, 551 /* Name */, LABEL_MASKS("%K", "%B"));
471
472   /*
473    * Video sort title is set to the channel number.
474    */
475   if (g_guiSettings.GetBool("filelists.ignorethewhensorting"))
476     items.AddSortMethod(SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE, 556 /* Title */, LABEL_MASKS("%K", "%B"));
477   else
478     items.AddSortMethod(SORT_METHOD_VIDEO_SORT_TITLE, 556 /* Title */, LABEL_MASKS("%K", "%B"));
479
480   return true;
481 }
482
483 bool CMythDirectory::GetDirectory(const CStdString& strPath, CFileItemList &items)
484 {
485   m_session = CMythSession::AquireSession(strPath);
486   if (!m_session)
487     return false;
488
489   m_dll = m_session->GetLibrary();
490   if (!m_dll)
491     return false;
492
493   CStdString base(strPath);
494   URIUtils::RemoveSlashAtEnd(base);
495
496   CURL url(strPath);
497   CStdString fileName = url.GetFileName();
498   URIUtils::RemoveSlashAtEnd(fileName);
499
500   if (fileName == "")
501   {
502     /*
503      * If we can't get the control then we can't connect to the backend. Don't even show any of the
504      * virtual folders as none of them will work. Without this check the "Browse" functionality
505      * when adding a myth:// source is way confusing as it shows folders so it looks like it has
506      * connected successfully when it in fact hasn't.
507      */
508     cmyth_conn_t control = m_session->GetControl();
509     if (!control)
510       return false;
511
512     CFileItemPtr item;
513
514     item.reset(new CFileItem(base + "/recordings/", true));
515     item->SetLabel(g_localizeStrings.Get(22015)); // All recordings
516     items.Add(item);
517
518     item.reset(new CFileItem(base + "/tvshows/", true));
519     item->SetLabel(g_localizeStrings.Get(20343)); // TV shows
520     items.Add(item);
521
522     item.reset(new CFileItem(base + "/movies/", true));
523     item->SetLabel(g_localizeStrings.Get(20342)); // Movies
524     items.Add(item);
525
526     item.reset(new CFileItem(base + "/channels/", true));
527     item->SetLabel(g_localizeStrings.Get(22018)); // Live channels
528     items.Add(item);
529
530     item.reset(new CFileItem(base + "/guide/", true));
531     item->SetLabel(g_localizeStrings.Get(22020)); // Guide
532     items.Add(item);
533
534     items.AddSortMethod(SORT_METHOD_NONE, 564 /* Type */, LABEL_MASKS("", "", "%L", "")); // No sorting, as added to list.
535
536     /*
537      * Clear the directory cache so the cached sub-folders are guaranteed to be accurate.
538      */
539     g_directoryCache.ClearSubPaths(base);
540
541     return true;
542   }
543   else if (fileName == "channels")
544     return GetChannels(base, items);
545   else if (fileName == "guide")
546     return GetGuide(base, items);
547   else if (fileName.Left(6) == "guide/")
548     return GetGuideForChannel(base, items, atoi(fileName.Mid(6)));
549   else if (fileName == "movies")
550     return GetRecordings(base, items, MOVIES);
551   else if (fileName == "recordings")
552     return GetRecordings(base, items);
553   else if (fileName == "tvshows")
554     return GetTvShowFolders(base, items);
555   else if (fileName.Left(8) == "tvshows/")
556     return GetRecordings(base, items, TV_SHOWS, fileName.Mid(8));
557   return false;
558 }
559
560 bool CMythDirectory::Exists(const char* strPath)
561 {
562   /*
563    * Return true for any virtual folders that are known to exist. Don't check for explicit
564    * existence using GetDirectory() as most methods will return true with empty content due to the
565    * way they are implemented - by iterating over all programs and filtering out content.
566    */
567   CURL url(strPath);
568   CStdString fileName = url.GetFileName();
569   URIUtils::RemoveSlashAtEnd(fileName);
570
571   if (fileName == ""
572   ||  fileName == "channels"
573   ||  fileName == "guide"
574   ||  fileName.Left(6) == "guide/"
575   ||  fileName == "movies"
576   ||  fileName == "recordings"
577   ||  fileName == "tvshows"
578   ||  fileName.Left(8) == "tvshows/")
579     return true;
580
581   return false;
582 }
583
584 bool CMythDirectory::IsVisible(const cmyth_proginfo_t program)
585 {
586   CStdString group = GetValue(m_dll->proginfo_recgroup(program));
587   unsigned long flags = m_dll->proginfo_flags(program);
588
589   /*
590    * Ignore programs that were recorded using "LiveTV" or that have been deleted via the
591    * "Auto Expire Instead of Delete Recording" option, which places the recording in the
592    * "Deleted" recording group for x days rather than deleting straight away.
593    *
594    * As of 0.24, when a recording is deleted using the Myth Protocol it is marked as "pending delete"
595    * using the program flags mask. It is then scheduled to be physically deleted in a detached
596    * thread. This means that a deleted recording can still appear in the list of all recordings.
597    * Recordings that are "pending delete" will have a program flag mask that matches
598    * FL_DELETEPENDING = 0x00000080.
599    */
600   return !(group.Equals("LiveTV") || group.Equals("Deleted") || flags & 0x00000080);
601 }
602
603 bool CMythDirectory::IsMovie(const cmyth_proginfo_t program)
604 {
605   /*
606    * The mythconverg.recordedprogram.programid field (if it exists) is a combination key where the first 2 characters map
607    * to the category_type and the rest is the key. From MythTV/release-0-21-fixes/mythtv/libs/libmythtv/programinfo.cpp
608    * "MV" = movie
609    * "EP" = series
610    * "SP" = sports
611    * "SH" = tvshow
612    *
613    * Based on MythTV usage it appears that the programid is only filled in for Movies though. Shame, could have used
614    * it for the other categories as well.
615    *
616    * mythconverg.recordedprogram.category_type contains the exact information that is needed. However, category_type
617    * isn't available through the libcmyth API. Since there is a direct correlation between the programid starting
618    * with "MV" and the category_type being "movie" that should work fine.
619    */
620
621   const int iMovieLength = g_advancedSettings.m_iMythMovieLength; // Minutes
622   if (iMovieLength > 0) // Use hack to identify movie based on length (used if EPG is dubious).
623     return GetValue(m_dll->proginfo_programid(program)).Left(2) == "MV"
624         || m_dll->proginfo_length_sec(program) > iMovieLength * 60; // Minutes to seconds
625   else
626     return GetValue(m_dll->proginfo_programid(program)).Left(2) == "MV";
627 }
628
629 bool CMythDirectory::IsTvShow(const cmyth_proginfo_t program)
630 {
631   /*
632    * There isn't enough information exposed by libcmyth to distinguish between an episode in a series and a
633    * one off TV show. See comment in IsMovie for more information.
634    *
635    * Return anything that isn't a movie as per any advanced setting override. This may result in a
636    * recorded TV Show only being shown in the Movies directory if it's something like a double
637    * episode.
638    */
639   return !IsMovie(program);
640 }
641
642 bool CMythDirectory::SupportsFileOperations(const CStdString& strPath)
643 {
644   CURL url(strPath);
645   CStdString filename = url.GetFileName();
646   URIUtils::RemoveSlashAtEnd(filename);
647   /*
648    * TV Shows directory has sub-folders so extra check is included so only files get the file
649    * operations.
650    */
651   return filename.Left(11) == "recordings/" ||
652          filename.Left(7)  == "movies/" ||
653         (filename.Left(8)  == "tvshows/" && URIUtils::GetExtension(filename) != "");
654 }
655
656 bool CMythDirectory::IsLiveTV(const CStdString& strPath)
657 {
658   CURL url(strPath);
659   return url.GetFileName().Left(9) == "channels/";
660 }