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.
10 // FileShoutcast.cpp: implementation of the CShoutcastFile class.
12 //////////////////////////////////////////////////////////////////////
14 #include "ShoutcastFile.h"
16 #include "FileCache.h"
18 #include "ServiceBroker.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"
37 using namespace XFILE
;
38 using namespace MUSIC_INFO
;
39 using namespace std::chrono_literals
;
41 CShoutcastFile::CShoutcastFile() :
42 IFile(), CThread("ShoutcastFile")
51 CShoutcastFile::~CShoutcastFile()
56 int64_t CShoutcastFile::GetPosition()
58 return m_file
.GetPosition()-m_discarded
;
61 int64_t CShoutcastFile::GetLength()
66 std::string
CShoutcastFile::DecodeToUTF8(const std::string
& str
)
68 std::string ret
= str
;
70 if (m_fileCharset
.empty())
71 g_charsetConverter
.unknownToUTF8(ret
);
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
);
83 bool CShoutcastFile::Open(const CURL
& 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");
95 bool result
= m_file
.Open(url2
);
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
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());
118 m_buffer
= new char[16*255];
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
);
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;
153 toRead
= std::min
<size_t>(uiBufSize
,m_metaint
-m_currint
);
155 toRead
= std::min
<size_t>(uiBufSize
,16*255);
156 toRead
= m_file
.Read(lpBuf
,toRead
);
162 int64_t CShoutcastFile::Seek(int64_t iFilePosition
, int iWhence
)
167 void CShoutcastFile::Close()
176 std::unique_lock
<CCriticalSection
> lock(m_tagSection
);
177 while (!m_tags
.empty())
184 bool CShoutcastFile::ExtractTagInfo(const char* buf
)
186 std::string strBuffer
= DecodeToUTF8(buf
);
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
203 std::string artistInfo
;
204 std::string coverURL
;
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
;
224 if (http
.Get(dataURL
.Get(), extData
))
226 const std::string contentType
= http
.GetHttpHeader().GetMimeType();
227 if (StringUtils::EqualsNoCase(contentType
, "application/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())
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];
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 "))
297 if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bShoutcastArt
)
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
);
315 void CShoutcastFile::ReadTruncated(char* buf2
, int size
)
320 int read
= m_file
.Read(buf
,size
);
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
);
335 return IFile::IoControl(control
, payload
);
338 void CShoutcastFile::Process()
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
);
355 CFileItem
* item
= new CFileItem(*front
.second
); // will be deleted by msg receiver
357 CServiceBroker::GetAppMessenger()->PostMsg(TMSG_UPDATE_CURRENT_ITEM
, 1, -1,
358 static_cast<void*>(item
));