2015-08-05 22:22:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
local ContentDirectory = {}
|
|
|
|
|
|
|
|
-- local dlnaprofile = 'DLNA.ORG_PN=MPEG_TS;DLNA.ORG_OP=00;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=0D100000000000000000000000000000'
|
|
|
|
local dlnaprofile = 'DLNA.ORG_PN=MPEG_TS;DLNA.ORG_OP=00;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=8D100000000000000000000000000000'
|
|
|
|
local dlnaschema = ' xmlns:dlna="urn:schemas-dlna-org:metadata-1-0"'
|
|
|
|
|
|
|
|
if DisableDLNA then
|
|
|
|
dlnaprofile = '*'
|
|
|
|
dlnaschema = ''
|
|
|
|
end
|
|
|
|
|
|
|
|
local Schema = 'xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1"'
|
|
|
|
|
|
|
|
local DIDLStart = '' -- '<?xml version="1.0" encoding="utf-8"?>'
|
|
|
|
..'<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/"'
|
|
|
|
..' xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"'
|
|
|
|
..' xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"'
|
|
|
|
-- .. dlnaschema
|
|
|
|
..'>'
|
|
|
|
local DIDLEnd = '</DIDL-Lite>'
|
|
|
|
|
|
|
|
-- State variables
|
|
|
|
local SystemUpdateID = 36
|
|
|
|
-- ---------------
|
|
|
|
----------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
local AllFolders = {}
|
|
|
|
local RootFolders = {}
|
|
|
|
local Folders = {}
|
|
|
|
local AllItems = {}
|
|
|
|
|
|
|
|
local i,f,vi,ai
|
|
|
|
|
|
|
|
----- Hierarchical Folders
|
|
|
|
|
|
|
|
RootFolders[#RootFolders+1] = { title = "All", id = "64" }
|
|
|
|
RootFolders[#RootFolders+1] = { title = "Audio", id = "1" }
|
|
|
|
RootFolders[#RootFolders+1] = { title = "Images", id = "3" }
|
|
|
|
RootFolders[#RootFolders+1] = { title = "Video", id = "2" }
|
|
|
|
for _,f in ipairs(RootFolders) do
|
|
|
|
f.VideoItems = {}
|
|
|
|
f.AudioItems = {}
|
|
|
|
f.ChildFolders = {}
|
|
|
|
AllFolders[f.id] = f
|
|
|
|
end
|
|
|
|
AllFolders["2"].ChildFolders = Folders
|
|
|
|
AllFolders["64"].ChildFolders = Folders
|
|
|
|
|
|
|
|
----
|
2016-01-07 17:50:38 +01:00
|
|
|
local file = io.open("/config/ChannelList.json")
|
|
|
|
if file then
|
|
|
|
local json = file:read("*a")
|
|
|
|
file:close()
|
|
|
|
local newdecoder = require("lunajson.decoder")
|
|
|
|
local decode = newdecoder()
|
|
|
|
local channellist = decode(json)
|
|
|
|
|
|
|
|
for _,group in ipairs(channellist.GroupList) do
|
|
|
|
local f = {}
|
|
|
|
f.id = "$"..tostring(#Folders)
|
|
|
|
f.VideoItems = {}
|
|
|
|
f.AudioItems = {}
|
|
|
|
f.ChildFolders = {}
|
|
|
|
f.title = string.gsub(group.Title,'&','&amp;')
|
|
|
|
f.title = string.gsub(f.title,'<','&lt;')
|
|
|
|
f.title = string.gsub(f.title,'>','&gt;')
|
|
|
|
table.insert(Folders,f)
|
|
|
|
-- table.insert(RootFolders,f)
|
|
|
|
AllFolders[f.id] = f
|
|
|
|
for _,channel in ipairs(group.ChannelList) do
|
|
|
|
local vi = {}
|
|
|
|
vi.id = f.id.."$"..tostring(#f.VideoItems)
|
|
|
|
vi.src = f.src
|
|
|
|
vi.parentID = f.id
|
|
|
|
vi.request = string.gsub(channel.Request,'&','&amp;')
|
|
|
|
vi.title = string.gsub(channel.Title,'&','&amp;')
|
|
|
|
vi.title = string.gsub(vi.title,'<','&lt;')
|
|
|
|
vi.title = string.gsub(vi.title,'>','&gt;')
|
|
|
|
table.insert(f.VideoItems,vi)
|
|
|
|
AllItems[vi.id] = vi
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
local db = require("DataBase")
|
|
|
|
for _,f in ipairs(db.SourceList) do
|
|
|
|
f.id = f.refid
|
|
|
|
f.VideoItems = {}
|
|
|
|
f.AudioItems = {}
|
|
|
|
f.ChildFolders = {}
|
|
|
|
table.insert(Folders,f)
|
|
|
|
-- table.insert(RootFolders,f)
|
|
|
|
AllFolders[f.id] = f
|
|
|
|
end
|
|
|
|
|
|
|
|
for _,vi in ipairs(db.ChannelList) do
|
|
|
|
f = AllFolders[vi.refid]
|
|
|
|
vi.id = f.id.."$"..tostring(#f.VideoItems)
|
|
|
|
vi.src = f.src
|
|
|
|
vi.parentID = f.id
|
|
|
|
vi.request = string.gsub(vi.request,'&','&amp;')
|
|
|
|
vi.title = string.gsub(vi.title,'&','&amp;')
|
|
|
|
vi.title = string.gsub(vi.title,'<','&lt;')
|
|
|
|
vi.title = string.gsub(vi.title,'>','&gt;')
|
|
|
|
table.insert(f.VideoItems,vi)
|
|
|
|
AllItems[vi.id] = vi
|
|
|
|
end
|
2015-08-05 22:22:06 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
----- Add Stream Folder
|
|
|
|
|
|
|
|
f = {}
|
|
|
|
f.id = "STRM"
|
|
|
|
f.title = "Current Streams"
|
|
|
|
f.VideoItems = {}
|
|
|
|
f.AudioItems = {}
|
|
|
|
f.ChildFolders = {}
|
|
|
|
table.insert(Folders,f)
|
|
|
|
AllFolders[f.id] = f
|
|
|
|
for i = 1,4,1 do
|
|
|
|
vi = { id = f.id.."$"..tostring(i-1), parentID = f.id, title = "Stream "..tostring(i), stream = tostring(i) }
|
|
|
|
table.insert(f.VideoItems,vi)
|
|
|
|
AllItems[vi.id] = vi
|
|
|
|
end
|
|
|
|
|
|
|
|
----------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
local function Folder(title,id,parentid,childCount)
|
|
|
|
local F = '<container id="'..id..'" parentID="'..parentid..'"'
|
|
|
|
-- ..' childCount="'..childCount..'"'
|
|
|
|
..' restricted="1"'
|
|
|
|
-- ..' searchable="1"'
|
|
|
|
..'>'
|
|
|
|
..'<dc:title>'..title..'</dc:title>'
|
|
|
|
..'<upnp:class>object.container.storageFolder</upnp:class>'
|
|
|
|
..'</container>'
|
|
|
|
return F
|
|
|
|
end
|
|
|
|
|
|
|
|
local function VideoItem(Host,Item,nCompat)
|
|
|
|
local rtspreq = ''
|
|
|
|
if Item.stream then
|
|
|
|
rtspreq = 'stream='..Item.stream
|
|
|
|
else
|
|
|
|
-- Some clients don't like a long request url, or an url with '&' in it
|
|
|
|
-- Fail them for now
|
|
|
|
-- if nCompat then
|
|
|
|
-- rtspreq = "stream_99"
|
|
|
|
-- elseif Item.src then
|
|
|
|
if Item.src then
|
|
|
|
rtspreq = '?src='..Item.src..'&amp;'..Item.request
|
|
|
|
else
|
|
|
|
rtspreq = '?'..Item.request
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local didl = '<item id="'..Item.id..'" parentID="'..Item.parentID
|
|
|
|
didl = didl .. '" restricted="1">'
|
|
|
|
..'<dc:title>'..Item.title..'</dc:title>'
|
|
|
|
..'<upnp:class>object.item.videoItem.videoBroadcast</upnp:class>'
|
|
|
|
if Item.channelNr then
|
|
|
|
didl = didl ..'<upnp:channelNr>'..Item.channelNr..'</upnp:channelNr>'
|
|
|
|
end
|
|
|
|
didl = didl ..'<res'
|
|
|
|
..' protocolInfo="rtsp-rtp-udp:*:video/mpeg:'..dlnaprofile..'">'
|
|
|
|
..'rtsp://'..Host..':554/'..rtspreq
|
|
|
|
..'</res>'
|
|
|
|
didl = didl ..'</item>'
|
|
|
|
return didl
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
local function BrowseChildren(client,Host,Request,nCompat)
|
|
|
|
local ObjectID = UPnP:GetRequestParam(Request,"ObjectID")
|
|
|
|
local BrowseFlag = UPnP:GetRequestParam(Request,"BrowseFlag")
|
|
|
|
local Filter = UPnP:GetRequestParam(Request,"Filter")
|
|
|
|
local StartingIndex = tonumber(UPnP:GetRequestParam(Request,"StartingIndex"))
|
|
|
|
local RequestedCount = tonumber(UPnP:GetRequestParam(Request,"RequestedCount"))
|
|
|
|
print("BrowseChildren",ObjectID,Filter,StartingIndex,RequestedCount)
|
|
|
|
|
|
|
|
local didl = DIDLStart;
|
|
|
|
local Error = 0
|
|
|
|
local NumberReturned = 0
|
|
|
|
local TotalMatches = 0
|
|
|
|
local UpdateID = SystemUpdateID
|
|
|
|
local f,vi,ai
|
|
|
|
|
|
|
|
if ObjectID == "0" then
|
|
|
|
if nCompat then
|
|
|
|
for _,f in ipairs(RootFolders) do
|
|
|
|
didl = didl..Folder(f.title,f.id,ObjectID,tostring(#f.VideoItems + #f.AudioItems + #f.ChildFolders))
|
|
|
|
NumberReturned = NumberReturned + 1
|
|
|
|
TotalMatches = TotalMatches +1
|
|
|
|
end
|
|
|
|
else
|
|
|
|
for _,f in ipairs(Folders) do
|
|
|
|
didl = didl..Folder(f.title,f.id,ObjectID,tostring(#f.VideoItems + #f.AudioItems + #f.ChildFolders))
|
|
|
|
NumberReturned = NumberReturned + 1
|
|
|
|
TotalMatches = TotalMatches +1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
else
|
|
|
|
local f = AllFolders[ObjectID]
|
|
|
|
if f then
|
|
|
|
|
|
|
|
local Index = 0
|
|
|
|
for i,cf in ipairs(f.ChildFolders) do
|
|
|
|
if Index >= StartingIndex and (RequestedCount == 0 or NumberReturned < RequestedCount) then
|
|
|
|
didl = didl..Folder(cf.title,cf.id,ObjectID,tostring(#cf.VideoItems + #cf.AudioItems + #cf.ChildFolders))
|
|
|
|
NumberReturned = NumberReturned + 1
|
|
|
|
end
|
|
|
|
Index = Index + 1
|
|
|
|
TotalMatches = TotalMatches +1
|
|
|
|
end
|
|
|
|
|
|
|
|
for i,vi in ipairs(f.VideoItems) do
|
|
|
|
if Index >= StartingIndex and (RequestedCount == 0 or NumberReturned < RequestedCount) then
|
|
|
|
didl = didl..VideoItem(Host,vi,nCompat)
|
|
|
|
NumberReturned = NumberReturned + 1
|
|
|
|
end
|
|
|
|
Index = Index + 1
|
|
|
|
TotalMatches = TotalMatches +1
|
|
|
|
end
|
|
|
|
|
|
|
|
else
|
|
|
|
Error = 710
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
didl = didl..DIDLEnd
|
|
|
|
print("Returned",StartingIndex,NumberReturned,TotalMatches,Error)
|
|
|
|
|
|
|
|
if Error == 0 then
|
|
|
|
local Args = { { n = "Result", v = didl }, { n = "NumberReturned", v = tostring(NumberReturned)},
|
|
|
|
{ n = "TotalMatches", v = tostring(TotalMatches)}, { n = "UpdateID", v = tostring(UpdateID) } }
|
|
|
|
UPnP:SendResponse(client,UPnP:CreateResponse(Schema,"Browse",Args))
|
|
|
|
else
|
|
|
|
UPnP:SendSoapError(client,Error)
|
|
|
|
end
|
|
|
|
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local function BrowseMetaData(client,Host,Request,nCompat)
|
|
|
|
local ObjectID = UPnP:GetRequestParam(Request,"ObjectID")
|
|
|
|
local didl = DIDLStart;
|
|
|
|
local UpdateID = SystemUpdateID
|
|
|
|
local Error = 0
|
|
|
|
|
|
|
|
if ObjectID == "0" then
|
|
|
|
local ChildCount = #Folders
|
|
|
|
if nCompat then ChildCount = #RootFolders end
|
|
|
|
didl = didl .. '<container id="1" parentID = "-1" childCount="'..tostring(ChildCount)..'" restricted="true">>'
|
|
|
|
.. '<:dc:title>OctopusNet</dc:title>'
|
|
|
|
..'<upnp:class>object.container.storageFolder</upnp:class>'
|
|
|
|
..'</container>'
|
|
|
|
else
|
|
|
|
local f = AllFolders[ObjectID]
|
|
|
|
if f then
|
|
|
|
didl = didl..Folder(f.title,f.id,ObjectID,tostring(#f.VideoItems + #f.AudioItems + #f.ChildFolders))
|
|
|
|
else
|
|
|
|
local item = AllItems[ObjectID]
|
|
|
|
if item then
|
|
|
|
didl = didl..VideoItem(Host,item,nCompat)
|
|
|
|
else
|
|
|
|
Error = 710
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
didl = didl..DIDLEnd
|
|
|
|
|
|
|
|
if Error == 0 then
|
|
|
|
local Args = { { n = "Result", v = didl }, { n = "NumberReturned", v = "1"},
|
|
|
|
{ n = "TotalMatches", v = "1"}, { n = "UpdateID", v = tostring(UpdateID) } }
|
|
|
|
UPnP:SendResponse(client,UPnP:CreateResponse(Schema,"Browse",Args))
|
|
|
|
else
|
|
|
|
UPnP:SendSoapError(client,Error)
|
|
|
|
end
|
|
|
|
|
|
|
|
didl = DIDLEnd;
|
|
|
|
end
|
|
|
|
|
|
|
|
local function Search(client,Host,Request,nCompat)
|
|
|
|
local ContainerID = UPnP:GetRequestParam(Request,"ContainerID")
|
|
|
|
local SearchCriteria = UPnP:GetRequestParam(Request,"SearchCriteria")
|
|
|
|
local Filter = UPnP:GetRequestParam(Request,"Filter")
|
|
|
|
local StartingIndex = tonumber(UPnP:GetRequestParam(Request,"StartingIndex"))
|
|
|
|
local RequestedCount = tonumber(UPnP:GetRequestParam(Request,"RequestedCount"))
|
|
|
|
local SortCriteria = UPnP:GetRequestParam(Request,"SortCriteria")
|
|
|
|
print(SearchCriteria,ContainerID,Filter,StartingIndex,RequestedCount,SortCriteria)
|
|
|
|
|
|
|
|
local didl = DIDLStart;
|
|
|
|
local Error = 0
|
|
|
|
local NumberReturned = 0
|
|
|
|
local TotalMatches = 0
|
|
|
|
local UpdateID = SystemUpdateID
|
|
|
|
|
|
|
|
if string.match(SearchCriteria,"videoItem") then
|
|
|
|
|
|
|
|
if ContainerID == "0" then
|
|
|
|
if not nCompat or nCompat ~= "WMP" then
|
|
|
|
local Index = 0
|
|
|
|
for _,vi in pairs(AllItems) do
|
|
|
|
if Index >= StartingIndex and (RequestedCount == 0 or NumberReturned < RequestedCount) then
|
|
|
|
didl = didl..VideoItem(Host,vi,nCompat)
|
|
|
|
NumberReturned = NumberReturned + 1
|
|
|
|
end
|
|
|
|
Index = Index + 1
|
|
|
|
TotalMatches = TotalMatches + 1
|
|
|
|
-- if nCompat and TotalMatches > 19 then break end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
else
|
|
|
|
local f = AllFolders[ContainerID]
|
|
|
|
if f then
|
|
|
|
local src = f.src
|
|
|
|
if src then src = "src="..src.."&" else src = "" end
|
|
|
|
|
|
|
|
local Index = 0
|
|
|
|
for _,vi in ipairs(f.VideoItems) do
|
|
|
|
if Index >= StartingIndex and (RequestedCount == 0 or NumberReturned < RequestedCount) then
|
|
|
|
didl = didl..VideoItem(Host,vi,nCompat)
|
|
|
|
NumberReturned = NumberReturned + 1
|
|
|
|
end
|
|
|
|
Index = Index + 1
|
|
|
|
TotalMatches = TotalMatches +1
|
|
|
|
end
|
|
|
|
else
|
|
|
|
Error = 710
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
didl = didl..DIDLEnd
|
|
|
|
-- didl = TestDidl
|
|
|
|
-- NumberReturned = 1
|
|
|
|
-- TotalMatches = 1
|
|
|
|
print("Returned",StartingIndex,NumberReturned,TotalMatches,Error)
|
|
|
|
|
|
|
|
|
|
|
|
if Error == 0 then
|
|
|
|
local Args = { { n = "Result", v = didl }, { n = "NumberReturned", v = tostring(NumberReturned)},
|
|
|
|
{ n = "TotalMatches", v = tostring(TotalMatches)}, { n = "UpdateID", v = tostring(UpdateID) } }
|
|
|
|
UPnP:SendResponse(client,UPnP:CreateResponse(Schema,"Search",Args))
|
|
|
|
else
|
|
|
|
UPnP:SendSoapError(client,Error)
|
|
|
|
end
|
|
|
|
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local function SendResult(client,Action,VarName,Result)
|
|
|
|
local Args = { { n = VarName, v = Result } }
|
|
|
|
UPnP:SendResponse(client,UPnP:CreateResponse(Schema,Action,Args))
|
|
|
|
end
|
|
|
|
|
|
|
|
function ContentDirectory:Invoke(client,Attributes,Request)
|
|
|
|
local Action = string.match(Attributes["SOAPACTION"],".+%#([%a%d%-_]+)")
|
|
|
|
local Host = tostring(Attributes["host"])
|
|
|
|
|
|
|
|
local Compability = nil
|
|
|
|
if Attributes["USER-AGENT"] then
|
|
|
|
if string.match(Attributes["USER-AGENT"],"Windows%-Media%-Player") then Compability = "WMP"
|
|
|
|
elseif string.match(Attributes["USER-AGENT"],"IPI%/1%.0") then Compability = "IPI"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
print("Compability=",Compability)
|
|
|
|
|
|
|
|
print(Host,"ContentDirectory",Action)
|
|
|
|
if Action == "Browse" then
|
|
|
|
local BrowseFlag = tostring(UPnP:GetRequestParam(Request,"BrowseFlag"))
|
|
|
|
if BrowseFlag == "BrowseDirectChildren" then
|
|
|
|
BrowseChildren(client,Host,Request,Compability)
|
|
|
|
elseif BrowseFlag == "BrowseMetadata" then
|
|
|
|
BrowseMetaData(client,Host,Request,Compability)
|
|
|
|
else
|
|
|
|
UPnP:SendSoapError(client,710)
|
|
|
|
end
|
|
|
|
elseif Action == "Search" then
|
|
|
|
Search(client,Host,Request,Compability)
|
|
|
|
-- elseif Action == "X_GetRemoteSharingStatus" then
|
|
|
|
-- SendResult(client,Action,"0")
|
|
|
|
elseif Action == "GetSortCapabilities" then
|
|
|
|
SendResult(client,Action,"SortCaps","dc:title,upnp:class,upnp:originalTrackNumber")
|
|
|
|
elseif Action == "GetSearchCapabilities" then
|
|
|
|
SendResult(client,Action,"SearchCaps","dc:title")
|
|
|
|
elseif Action == "GetSystemUpdateID" then
|
|
|
|
SendResult(client,Action,"Id","1")
|
|
|
|
else
|
|
|
|
UPnP:SendSoapError(client,401)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function ContentDirectory:Subscribe(client,callback,timeout)
|
|
|
|
local r = "HTTP/1.1 200 OK\r\n"
|
|
|
|
.. 'Content-Type: text/xml; charset="utf-8"\r\n'
|
|
|
|
.. "Server: "..UPnP.Server.."\r\n"
|
|
|
|
.. "SID: uuid:50c95800-e839-4b96-b7ae-779d989e1399\r\n"
|
|
|
|
.. "Timeout: Second-1800\r\n"
|
|
|
|
.. "Content-Length: 0\r\n"
|
|
|
|
.. "Connection: close\r\n"
|
|
|
|
.. "EXT:\r\n"
|
|
|
|
.. "\r\n"
|
|
|
|
client:send(r)
|
|
|
|
|
|
|
|
local ipaddr,port = client:getpeername()
|
|
|
|
local Args = { { n = "TransferIDs", v = "" }, { n = "SystemUpdateID", v = tostring(SystemUpdateID) } }
|
|
|
|
UPnP:SendEvent(callback,"50c95800-e839-4b96-b7ae-779d989e1399",0,Args)
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
function ContentDirectory:Renew(client,sid,timeout)
|
|
|
|
local r = "HTTP/1.1 200 OK\r\n"
|
|
|
|
.. 'Content-Type: text/xml; charset="utf-8"\r\n'
|
|
|
|
.. "Server: "..UPnP.Server.."\r\n"
|
|
|
|
.. "SID: uuid:50c95800-e839-4b96-b7ae-779d989e1399\r\n"
|
|
|
|
.. "Timeout: Second-1800\r\n"
|
|
|
|
.. "Content-Length: 0\r\n"
|
|
|
|
.. "Connection: close\r\n"
|
|
|
|
.. "EXT:\r\n"
|
|
|
|
.. "\r\n"
|
|
|
|
client:send(r)
|
|
|
|
end
|
|
|
|
|
|
|
|
function ContentDirectory:Unsubscribe(client,sid)
|
|
|
|
local r = "HTTP/1.1 200 OK\r\n"
|
|
|
|
.. 'Content-Type: text/xml; charset="utf-8"\r\n'
|
|
|
|
.. "Server: "..UPnP.Server.."\r\n"
|
|
|
|
.. "Content-Length: 0\r\n"
|
|
|
|
.. "Connection: close\r\n"
|
|
|
|
.. "EXT:\r\n"
|
|
|
|
.. "\r\n"
|
|
|
|
client:send(r)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function ContentDirectory:Description()
|
|
|
|
t = ""
|
|
|
|
local f = io.open("ContentDirectory.xml","r")
|
|
|
|
if not f then os.exit() end
|
|
|
|
while true do
|
|
|
|
local line = f:read()
|
|
|
|
if not line then break end
|
|
|
|
t = t .. line .. "\r\n"
|
|
|
|
end
|
|
|
|
f:close()
|
|
|
|
return t
|
|
|
|
end
|
|
|
|
|
|
|
|
return ContentDirectory
|