Merge pull request #25808 from CastagnaIT/fix_url_parse
[xbmc.git] / xbmc / filesystem / ShoutcastFile.cpp
blob861c001e01f76202018a55239366a70029506f2a
1 /*
2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
7 */
10 // FileShoutcast.cpp: implementation of the CShoutcastFile class.
12 //////////////////////////////////////////////////////////////////////
14 #include "ShoutcastFile.h"
16 #include "FileCache.h"
17 #include "FileItem.h"
18 #include "ServiceBroker.h"
19 #include "URL.h"
20 #include "filesystem/CurlFile.h"
21 #include "messaging/ApplicationMessenger.h"
22 #include "music/tags/MusicInfoTag.h"
23 #include "settings/AdvancedSettings.h"
24 #include "settings/SettingsComponent.h"
25 #include "threads/SingleLock.h"
26 #include "utils/CharsetConverter.h"
27 #include "utils/HTMLUtil.h"
28 #include "utils/JSONVariantParser.h"
29 #include "utils/RegExp.h"
30 #include "utils/StringUtils.h"
31 #include "utils/UrlOptions.h"
33 #include <climits>
34 #include <memory>
35 #include <mutex>
37 using namespace XFILE;
38 using namespace MUSIC_INFO;
39 using namespace std::chrono_literals;
41 CShoutcastFile::CShoutcastFile() :
42 IFile(), CThread("ShoutcastFile")
44 m_discarded = 0;
45 m_currint = 0;
46 m_buffer = NULL;
47 m_cacheReader = NULL;
48 m_metaint = 0;
51 CShoutcastFile::~CShoutcastFile()
53 Close();
56 int64_t CShoutcastFile::GetPosition()
58 return m_file.GetPosition()-m_discarded;
61 int64_t CShoutcastFile::GetLength()
63 return 0;
66 std::string CShoutcastFile::DecodeToUTF8(const std::string& str)
68 std::string ret = str;
70 if (m_fileCharset.empty())
71 g_charsetConverter.unknownToUTF8(ret);
72 else
73 g_charsetConverter.ToUtf8(m_fileCharset, str, ret);
75 std::wstring wBuffer, wConverted;
76 g_charsetConverter.utf8ToW(ret, wBuffer, false);
77 HTML::CHTMLUtil::ConvertHTMLToW(wBuffer, wConverted);
78 g_charsetConverter.wToUTF8(wConverted, ret);
80 return ret;
83 bool CShoutcastFile::Open(const CURL& url)
85 CURL url2(url);
86 url2.SetProtocolOptions(url2.GetProtocolOptions()+"&noshout=true&Icy-MetaData=1");
87 if (url.GetProtocol() == "shouts")
88 url2.SetProtocol("https");
89 else if (url.GetProtocol() == "shout")
90 url2.SetProtocol("http");
92 std::string icyTitle;
93 std::string icyGenre;
95 bool result = m_file.Open(url2);
96 if (result)
98 m_fileCharset = m_file.GetProperty(XFILE::FILE_PROPERTY_CONTENT_CHARSET);
100 icyTitle = m_file.GetHttpHeader().GetValue("icy-name");
101 if (icyTitle.empty())
102 icyTitle = m_file.GetHttpHeader().GetValue("ice-name"); // icecast
103 if (icyTitle == "This is my server name") // Handle badly set up servers
104 icyTitle.clear();
106 icyTitle = DecodeToUTF8(icyTitle);
108 icyGenre = m_file.GetHttpHeader().GetValue("icy-genre");
109 if (icyGenre.empty())
110 icyGenre = m_file.GetHttpHeader().GetValue("ice-genre"); // icecast
112 icyGenre = DecodeToUTF8(icyGenre);
114 m_metaint = atoi(m_file.GetHttpHeader().GetValue("icy-metaint").c_str());
115 if (!m_metaint)
116 m_metaint = -1;
118 m_buffer = new char[16*255];
120 if (result)
122 std::unique_lock<CCriticalSection> lock(m_tagSection);
124 m_masterTag = std::make_shared<CMusicInfoTag>();
125 m_masterTag->SetStationName(icyTitle);
126 m_masterTag->SetGenre(icyGenre);
127 m_masterTag->SetLoaded(true);
129 m_tags.emplace(1, m_masterTag);
130 m_tagChange.Set();
133 return result;
136 ssize_t CShoutcastFile::Read(void* lpBuf, size_t uiBufSize)
138 if (uiBufSize > SSIZE_MAX)
139 uiBufSize = SSIZE_MAX;
141 if (m_currint >= m_metaint && m_metaint > 0)
143 unsigned char header;
144 m_file.Read(&header,1);
145 ReadTruncated(m_buffer, header*16);
146 ExtractTagInfo(m_buffer);
147 m_discarded += header*16+1;
148 m_currint = 0;
151 ssize_t toRead;
152 if (m_metaint > 0)
153 toRead = std::min<size_t>(uiBufSize,m_metaint-m_currint);
154 else
155 toRead = std::min<size_t>(uiBufSize,16*255);
156 toRead = m_file.Read(lpBuf,toRead);
157 if (toRead > 0)
158 m_currint += toRead;
159 return toRead;
162 int64_t CShoutcastFile::Seek(int64_t iFilePosition, int iWhence)
164 return -1;
167 void CShoutcastFile::Close()
169 StopThread();
170 delete[] m_buffer;
171 m_buffer = NULL;
172 m_file.Close();
173 m_title.clear();
176 std::unique_lock<CCriticalSection> lock(m_tagSection);
177 while (!m_tags.empty())
178 m_tags.pop();
179 m_masterTag.reset();
180 m_tagChange.Set();
184 bool CShoutcastFile::ExtractTagInfo(const char* buf)
186 std::string strBuffer = DecodeToUTF8(buf);
188 bool result = false;
190 CRegExp reTitle(true);
191 reTitle.RegComp("StreamTitle=\'(.*?)\';");
193 if (reTitle.RegFind(strBuffer.c_str()) != -1)
195 const std::string newtitle = reTitle.GetMatch(1);
197 result = (m_title != newtitle);
198 if (result) // track has changed
200 m_title = newtitle;
202 std::string title;
203 std::string artistInfo;
204 std::string coverURL;
206 CRegExp reURL(true);
207 reURL.RegComp("StreamUrl=\'(.*?)\';");
208 bool haveStreamUrlData =
209 (reURL.RegFind(strBuffer.c_str()) != -1) && !reURL.GetMatch(1).empty();
211 if (haveStreamUrlData) // track has changed and extra metadata might be available
213 const std::string streamUrlData = reURL.GetMatch(1);
214 if (StringUtils::StartsWithNoCase(streamUrlData, "http://") ||
215 StringUtils::StartsWithNoCase(streamUrlData, "https://"))
217 // Bauer Media Radio listenapi null event to erase current data
218 if (!StringUtils::EndsWithNoCase(streamUrlData, "eventdata/-1"))
220 const CURL dataURL(streamUrlData);
221 XFILE::CCurlFile http;
222 std::string extData;
224 if (http.Get(dataURL.Get(), extData))
226 const std::string contentType = http.GetHttpHeader().GetMimeType();
227 if (StringUtils::EqualsNoCase(contentType, "application/json"))
229 CVariant json;
230 if (CJSONVariantParser::Parse(extData, json))
232 // Check for Bauer Media Radio listenapi meta data.
233 // Example: StreamUrl='https://listenapi.bauerradio.com/api9/eventdata/58431417'
234 artistInfo = json["eventSongArtist"].asString();
235 title = json["eventSongTitle"].asString();
236 coverURL = json["eventImageUrl"].asString();
242 else if (StringUtils::StartsWithNoCase(streamUrlData, "&"))
244 // Check for SAM Cast meta data.
245 // Example: StreamUrl='&artist=RECLAM&title=BOLORDURAN%2017&album=&duration=17894&songtype=S&overlay=no&buycd=&website=&picture='
247 CUrlOptions urlOptions(streamUrlData);
248 const CUrlOptions::UrlOptions& options = urlOptions.GetOptions();
250 auto it = options.find("artist");
251 if (it != options.end())
252 artistInfo = (*it).second.asString();
254 it = options.find("title");
255 if (it != options.end())
256 title = (*it).second.asString();
258 it = options.find("picture");
259 if (it != options.end())
261 coverURL = (*it).second.asString();
262 if (!coverURL.empty())
264 // Check value being a URL (not just a file name)
265 const CURL url(coverURL);
266 if (url.GetProtocol().empty())
267 coverURL.clear();
273 if (artistInfo.empty() || title.empty())
275 // Most stations supply StreamTitle in format "artist - songtitle"
276 const std::vector<std::string> tokens = StringUtils::Split(newtitle, " - ");
277 if (tokens.size() == 2)
279 if (artistInfo.empty())
280 artistInfo = tokens[0];
282 if (title.empty())
283 title = tokens[1];
285 else
287 if (title.empty())
289 // Do not display Bauer Media Radio SteamTitle values to mark start/stop of ad breaks.
290 if (!StringUtils::StartsWithNoCase(newtitle, "START ADBREAK ") &&
291 !StringUtils::StartsWithNoCase(newtitle, "STOP ADBREAK "))
292 title = newtitle;
297 if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bShoutcastArt)
298 coverURL.clear();
300 std::unique_lock<CCriticalSection> lock(m_tagSection);
302 const std::shared_ptr<CMusicInfoTag> tag = std::make_shared<CMusicInfoTag>(*m_masterTag);
303 tag->SetArtist(artistInfo);
304 tag->SetTitle(title);
305 tag->SetStationArt(coverURL);
307 m_tags.emplace(m_file.GetPosition(), tag);
308 m_tagChange.Set();
312 return result;
315 void CShoutcastFile::ReadTruncated(char* buf2, int size)
317 char* buf = buf2;
318 while (size > 0)
320 int read = m_file.Read(buf,size);
321 size -= read;
322 buf += read;
326 int CShoutcastFile::IoControl(EIoControl control, void* payload)
328 if (control == IOCTRL_SET_CACHE && m_cacheReader == nullptr)
330 std::unique_lock<CCriticalSection> lock(m_tagSection);
331 m_cacheReader = static_cast<CFileCache*>(payload);
332 Create();
335 return IFile::IoControl(control, payload);
338 void CShoutcastFile::Process()
340 while (!m_bStop)
342 if (m_tagChange.Wait(500ms))
344 std::unique_lock<CCriticalSection> lock(m_tagSection);
345 while (!m_bStop && !m_tags.empty())
347 const TagInfo& front = m_tags.front();
348 if (m_cacheReader->GetPosition() < front.first) // tagpos
350 CSingleExit ex(m_tagSection);
351 CThread::Sleep(20ms);
353 else
355 CFileItem* item = new CFileItem(*front.second); // will be deleted by msg receiver
356 m_tags.pop();
357 CServiceBroker::GetAppMessenger()->PostMsg(TMSG_UPDATE_CURRENT_ITEM, 1, -1,
358 static_cast<void*>(item));