Actions

Module

VideoGallery: Difference between revisions

From Dune Awakening DB

Created page with "-- Module:VideoGallery -- Handles video gallery display with database integration local p = {} -- Database connection function local function getDB() return mw.ext.LuaSQLite.open('duneawakening') end -- Convert seconds to MM:SS format local function formatDuration(seconds) if not seconds or seconds == 0 then return "" end local minutes = math.floor(seconds / 60) local secs = seconds % 60 return string.format("%d:%02d", minutes, secs) en..."
 
mNo edit summary
 
(One intermediate revision by the same user not shown)
Line 1: Line 1:
-- Module:VideoGallery
-- Module:VideoGallery
-- Handles video gallery display with database integration
-- Lua module for rendering video gallery sections


local p = {}
local p = {}


-- Database connection function
-- Helper function to escape quotes
local function getDB()
local function escapeQuotes(str)
     return mw.ext.LuaSQLite.open('duneawakening')
     if not str then return '' end
    return str:gsub('"', '"'):gsub("'", ''')
end
end


-- Convert seconds to MM:SS format
-- Helper function to format duration
local function formatDuration(seconds)
local function formatDuration(seconds)
     if not seconds or seconds == 0 then
     if not seconds or seconds == 0 then return '' end
        return ""
    end
     local minutes = math.floor(seconds / 60)
     local minutes = math.floor(seconds / 60)
     local secs = seconds % 60
     local secs = seconds % 60
Line 19: Line 18:
end
end


-- Get video thumbnail URL
-- Render videos for a specific category
local function getVideoThumbnail(youtubeId)
function p.renderCategoryVideos(frame)
     -- YouTube thumbnail URL pattern
     local category = frame.args.category or 'Featured'
    return string.format("https://img.youtube.com/vi/%s/mqdefault.jpg", youtubeId)
    local html = ''
end
 
-- Parse video notes from markdown-style text
local function parseVideoNotes(notes)
    if not notes or notes == "" then
        return "<p>No notes available for this video.</p>"
    end
      
      
     -- Convert markdown-style formatting
     -- Get unique purposes for this category
    -- Headers: ### becomes <h3>
     local purposes = mw.ext.externalData.getExternalData{
    notes = notes:gsub("###%s*([^\n]+)", "<h3>%1</h3>")
         source = 'externaldb',
   
         from = 'data_videos',
    -- Lists: lines starting with - become <li>
         data = 'purpose=purpose',
    notes = notes:gsub("\n%-%s*([^\n]+)", "\n<li>%1</li>")
         where = "primary_tag='" .. category .. "' AND visibility='public' AND purpose IS NOT NULL",
   
         group_by = 'purpose',
    -- Wrap consecutive <li> items in <ul>
         order_by = 'purpose'
    notes = notes:gsub("(<li>.-</li>)\n(<li>)", "%1\n<ul>\n%2")
    notes = notes:gsub("(</li>)\n([^<])", "%1\n</ul>\n%2")
   
    -- Line breaks
    notes = notes:gsub("\n\n", "</p><p>")
    notes = "<p>" .. notes .. "</p>"
   
    return notes
end
 
-- Main function to render videos for a category
function p.renderVideos(frame)
     local category = frame.args.category or "featured"
    local db = getDB()
   
    -- Define tag mappings for each category
    local categoryTags = {
         featured = "Featured",
         leveling = "Leveling",
         crafting = "Crafting",
         building = "Building",
         pve = "PVE",
         pvp = "PVP"
     }
     }
      
      
    local tagName = categoryTags[category] or "Featured"
     -- Loop through each purpose and create a section
   
     if purposes and purposes.purpose then
     -- Query to get videos by tag and group by purpose
         for i, purpose in ipairs(purposes.purpose) do
     local query = [[
             html = html .. '<div class="video-section">\n'
         SELECT DISTINCT
             html = html .. '  <div class="video-section-header">\n'
             v.video_id,
             html = html .. '    <h3 class="section-title">' .. purpose .. '</h3>\n'
             v.youtube_id,
             html = html .. '  </div>\n'
             v.title,
            html = html .. '  <div class="video-grid">\n'
            v.channel_title,
           
            v.duration_sec,
            -- Get videos for this purpose
            v.purpose,
             local videos = mw.ext.externalData.getExternalData{
            v.description,
                source = 'externaldb',
            v.video_notes,
                from = 'data_videos',
             v.published_at
                data = 'video_id=video_id,youtube_id=youtube_id,title=title,channel_title=channel_title,channel_id=channel_id,author=author,published_at=published_at,duration_sec=duration_sec,purpose=purpose,primary_tag=primary_tag,secondary_tag=secondary_tag,description=description,video_notes=video_notes,video_internal_link=video_internal_link',
        FROM videos v
                where = "primary_tag='" .. category .. "' AND purpose='" .. purpose .. "' AND visibility='public'",
        JOIN video_tags vt ON v.video_id = vt.video_id
                order_by = 'published_at DESC'
        JOIN tags t ON vt.tag_id = t.tag_id
            }
        WHERE t.tag_name = ?
        ORDER BY v.purpose, v.published_at DESC
    ]]
   
    local stmt = db:prepare(query)
    stmt:bind_values(tagName)
   
    local videos = {}
    local purposes = {}
    local purposeOrder = {}
   
    -- Group videos by purpose
    for row in stmt:rows() do
        local purpose = row.purpose or "General"
       
        if not videos[purpose] then
             videos[purpose] = {}
            table.insert(purposeOrder, purpose)
        end
       
        table.insert(videos[purpose], {
            video_id = row.video_id,
            youtube_id = row.youtube_id,
            title = row.title,
            channel_title = row.channel_title,
            duration = formatDuration(row.duration_sec),
            duration_sec = row.duration_sec,
            description = row.description,
            video_notes = row.video_notes,
            published_at = row.published_at
        })
    end
   
    stmt:finalize()
   
    -- Build HTML output
    local output = {}
   
    -- Special ordering for certain categories
    local purposeOrderMap = {
        leveling = {
            "Leveling 1-20",
            "Leveling 21-40",
            "Leveling 41-60",
            "Leveling Tips",
            "General"
        },
        crafting = {
            "Basic Crafting",
            "Advanced Crafting",
            "Crafting Stations",
            "Crafting Tips",
            "General"
        },
        building = {
            "Base Building",
            "Advanced Structures",
            "Defense Building",
            "Building Tips",
            "General"
        }
    }
   
    -- Use custom order if available
    if purposeOrderMap[category] then
        purposeOrder = purposeOrderMap[category]
    end
   
    -- Render each purpose section
    for _, purpose in ipairs(purposeOrder) do
        if videos[purpose] then
            table.insert(output, string.format([[
<div class="video-section">
    <div class="video-section-header">
        <h3 class="section-title">%s</h3>
    </div>
    <div class="video-grid">]], purpose))
              
              
             -- Render video cards
             -- Render each video card
             for _, video in ipairs(videos[purpose]) do
             if videos and videos.video_id then
                local thumbnail = getVideoThumbnail(video.youtube_id)
                for j, video_id in ipairs(videos.video_id) do
               
                    local duration = formatDuration(tonumber(videos.duration_sec[j] or 0))
                table.insert(output, string.format([[
                   
        <div class="video-card" data-video-id="%s" data-youtube-id="%s">
                    html = html .. '    <div class="video-card" '
            <div class="video-thumbnail" style="background-image: url('%s');">
                    html = html .. 'data-video-id="' .. video_id .. '" '
                <span class="video-duration">%s</span>
                    html = html .. 'data-youtube-id="' .. (videos.youtube_id[j] or '') .. '" '
            </div>
                    html = html .. 'data-title="' .. escapeQuotes(videos.title[j] or '') .. '" '
            <div class="video-info">
                    html = html .. 'data-channel="' .. escapeQuotes(videos.channel_title[j] or '') .. '" '
                <div class="video-title">%s</div>
                    html = html .. 'data-channel-id="' .. (videos.channel_id[j] or '') .. '" '
                <div class="video-channel">%s</div>
                    html = html .. 'data-author="' .. escapeQuotes(videos.author[j] or '') .. '" '
            </div>
                    html = html .. 'data-published="' .. (videos.published_at[j] or '') .. '" '
        </div>]],
                    html = html .. 'data-duration="' .. (videos.duration_sec[j] or '') .. '" '
                    video.video_id,
                    html = html .. 'data-purpose="' .. escapeQuotes(videos.purpose[j] or '') .. '" '
                    video.youtube_id,
                    html = html .. 'data-primary-tag="' .. (videos.primary_tag[j] or '') .. '" '
                    thumbnail,
                    html = html .. 'data-secondary-tag="' .. (videos.secondary_tag[j] or '') .. '" '
                    video.duration,
                    html = html .. 'data-description="' .. escapeQuotes(videos.description[j] or '') .. '" '
                    mw.text.encode(video.title),
                    html = html .. 'data-notes="' .. escapeQuotes(videos.video_notes[j] or '') .. '" '
                    mw.text.encode(video.channel_title)
                    html = html .. 'data-internal-link="' .. escapeQuotes(videos.video_internal_link[j] or '') .. '">\n'
                 ))
                   
                    html = html .. '      <div class="video-thumbnail" style="background-image: url(\'https://img.youtube.com/vi/' .. (videos.youtube_id[j] or '') .. '/mqdefault.jpg\');">\n'
                    if duration ~= '' then
                        html = html .. '        <span class="video-duration">' .. duration .. '</span>\n'
                    end
                    html = html .. '      </div>\n'
                    html = html .. '      <div class="video-info">\n'
                    html = html .. '        <div class="video-title">' .. (videos.title[j] or '') .. '</div>\n'
                    html = html .. '        <div class="video-channel">' .. (videos.channel_title[j] or '') .. '</div>\n'
                    html = html .. '      </div>\n'
                    html = html .. '    </div>\n'
                 end
             end
             end
              
              
             table.insert(output, [[
             html = html .. '  </div>\n'
    </div>
            html = html .. '</div>\n'
</div>]])
         end
         end
     end
     end
      
      
     db:close()
     return html
   
    if #output == 0 then
        return '<div class="video-section"><p style="text-align: center; color: #E3BB7A;">No videos found for this category.</p></div>'
    end
   
    return table.concat(output, "\n")
end
end


-- Function to get all video data as JSON for JavaScript
-- Render featured videos (shows videos from all categories)
function p.getVideoData(frame)
function p.renderFeaturedVideos(frame)
     local db = getDB()
     local html = ''
      
      
     local query = [[
    -- Get unique purposes from all categories
        SELECT
     local purposes = mw.ext.externalData.getExternalData{
            v.video_id,
        source = 'externaldb',
            v.youtube_id,
        from = 'data_videos',
            v.title,
        data = 'purpose=purpose',
            v.channel_title,
        where = "visibility='public' AND purpose IS NOT NULL",
            v.author,
        group_by = 'purpose',
            v.duration_sec,
         order_by = 'purpose',
            v.purpose,
         limit = 10  -- Limit featured to first 10 purposes
            v.description,
     }
            v.video_notes,
            v.published_at,
            GROUP_CONCAT(t.tag_name) as tags
        FROM videos v
         LEFT JOIN video_tags vt ON v.video_id = vt.video_id
         LEFT JOIN tags t ON vt.tag_id = t.tag_id
        GROUP BY v.video_id
        ORDER BY v.published_at DESC
     ]]
      
      
     local stmt = db:prepare(query)
     -- Loop through each purpose and create a section
    local videoData = {}
    if purposes and purposes.purpose then
   
        for i, purpose in ipairs(purposes.purpose) do
    for row in stmt:rows() do
            html = html .. '<div class="video-section">\n'
        local tags = {}
            html = html .. '  <div class="video-section-header">\n'
        if row.tags then
            html = html .. '    <h3 class="section-title">' .. purpose .. '</h3>\n'
            for tag in string.gmatch(row.tags, "[^,]+") do
            html = html .. '  </div>\n'
                table.insert(tags, mw.text.trim(tag))
            html = html .. '  <div class="video-grid">\n'
           
            -- Get videos for this purpose
            local videos = mw.ext.externalData.getExternalData{
                source = 'externaldb',
                from = 'data_videos',
                data = 'video_id=video_id,youtube_id=youtube_id,title=title,channel_title=channel_title,channel_id=channel_id,author=author,published_at=published_at,duration_sec=duration_sec,purpose=purpose,primary_tag=primary_tag,secondary_tag=secondary_tag,description=description,video_notes=video_notes,video_internal_link=video_internal_link',
                where = "purpose='" .. purpose .. "' AND visibility='public'",
                order_by = 'published_at DESC',
                limit = 4  -- Limit to 4 videos per purpose in featured
            }
           
            -- Render each video card
            if videos and videos.video_id then
                for j, video_id in ipairs(videos.video_id) do
                    local duration = formatDuration(tonumber(videos.duration_sec[j] or 0))
                   
                    html = html .. '    <div class="video-card" '
                    html = html .. 'data-video-id="' .. video_id .. '" '
                    html = html .. 'data-youtube-id="' .. (videos.youtube_id[j] or '') .. '" '
                    html = html .. 'data-title="' .. escapeQuotes(videos.title[j] or '') .. '" '
                    html = html .. 'data-channel="' .. escapeQuotes(videos.channel_title[j] or '') .. '" '
                    html = html .. 'data-channel-id="' .. (videos.channel_id[j] or '') .. '" '
                    html = html .. 'data-author="' .. escapeQuotes(videos.author[j] or '') .. '" '
                    html = html .. 'data-published="' .. (videos.published_at[j] or '') .. '" '
                    html = html .. 'data-duration="' .. (videos.duration_sec[j] or '') .. '" '
                    html = html .. 'data-purpose="' .. escapeQuotes(videos.purpose[j] or '') .. '" '
                    html = html .. 'data-primary-tag="' .. (videos.primary_tag[j] or '') .. '" '
                    html = html .. 'data-secondary-tag="' .. (videos.secondary_tag[j] or '') .. '" '
                    html = html .. 'data-description="' .. escapeQuotes(videos.description[j] or '') .. '" '
                    html = html .. 'data-notes="' .. escapeQuotes(videos.video_notes[j] or '') .. '" '
                    html = html .. 'data-internal-link="' .. escapeQuotes(videos.video_internal_link[j] or '') .. '">\n'
                   
                    html = html .. '      <div class="video-thumbnail" style="background-image: url(\'https://img.youtube.com/vi/' .. (videos.youtube_id[j] or '') .. '/mqdefault.jpg\');">\n'
                    if duration ~= '' then
                        html = html .. '        <span class="video-duration">' .. duration .. '</span>\n'
                    end
                    html = html .. '      </div>\n'
                    html = html .. '      <div class="video-info">\n'
                    html = html .. '        <div class="video-title">' .. (videos.title[j] or '') .. '</div>\n'
                    html = html .. '        <div class="video-channel">' .. (videos.channel_title[j] or '') .. '</div>\n'
                    html = html .. '      </div>\n'
                    html = html .. '    </div>\n'
                end
             end
             end
           
            html = html .. '  </div>\n'
            html = html .. '</div>\n'
         end
         end
       
        videoData[tostring(row.video_id)] = {
            video_id = row.video_id,
            youtube_id = row.youtube_id,
            title = row.title,
            channel_title = row.channel_title,
            author = row.author,
            duration = formatDuration(row.duration_sec),
            duration_sec = row.duration_sec,
            purpose = row.purpose,
            description = row.description,
            video_notes = parseVideoNotes(row.video_notes),
            published_at = row.published_at,
            tags = tags
        }
     end
     end
      
      
    stmt:finalize()
     return html
    db:close()
   
     return mw.text.jsonEncode(videoData)
end
 
-- Function to render a single video player (called via AJAX)
function p.renderVideoPlayer(frame)
    local videoId = frame.args.video_id or frame.args[1]
   
    if not videoId then
        return '<div class="error">No video ID provided</div>'
    end
   
    local db = getDB()
   
    local query = [[
        SELECT
            v.*,
            GROUP_CONCAT(t.tag_name) as tags
        FROM videos v
        LEFT JOIN video_tags vt ON v.video_id = vt.video_id
        LEFT JOIN tags t ON vt.tag_id = t.tag_id
        WHERE v.video_id = ?
        GROUP BY v.video_id
    ]]
   
    local stmt = db:prepare(query)
    stmt:bind_values(videoId)
   
    local video = nil
    for row in stmt:rows() do
        video = row
        break
    end
   
    stmt:finalize()
    db:close()
   
    if not video then
        return '<div class="error">Video not found</div>'
    end
   
    -- Parse published date
    local publishedDate = video.published_at:match("(%d%d%d%d%-%d%d%-%d%d)")
   
    -- Build player HTML
    local output = string.format([[
<div class="video-embed-container">
    <iframe src="https://www.youtube.com/embed/%s?rel=0&modestbranding=1"
            frameborder="0"
            allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
            allowfullscreen>
    </iframe>
</div>
 
<div class="video-details-header">
    <h2 class="video-main-title">%s</h2>
    <div class="video-meta">
        <span class="video-author">👤 %s</span>
        <span class="video-date">📅 %s</span>
    </div>
</div>
 
<div class="video-notes-section">
    <div class="notes-header">Video Notes</div>
    <div class="notes-content">
        %s
    </div>
</div>]],
        video.youtube_id,
        mw.text.encode(video.title),
        mw.text.encode(video.author or video.channel_title),
        publishedDate,
        parseVideoNotes(video.video_notes)
    )
   
    return output
end
end


return p
return p

Latest revision as of 17:43, 2 June 2025

Documentation for this module may be created at Module:VideoGallery/doc

-- Module:VideoGallery
-- Lua module for rendering video gallery sections

local p = {}

-- Helper function to escape quotes
local function escapeQuotes(str)
    if not str then return '' end
    return str:gsub('"', '&quot;'):gsub("'", '&#39;')
end

-- Helper function to format duration
local function formatDuration(seconds)
    if not seconds or seconds == 0 then return '' end
    local minutes = math.floor(seconds / 60)
    local secs = seconds % 60
    return string.format("%d:%02d", minutes, secs)
end

-- Render videos for a specific category
function p.renderCategoryVideos(frame)
    local category = frame.args.category or 'Featured'
    local html = ''
    
    -- Get unique purposes for this category
    local purposes = mw.ext.externalData.getExternalData{
        source = 'externaldb',
        from = 'data_videos',
        data = 'purpose=purpose',
        where = "primary_tag='" .. category .. "' AND visibility='public' AND purpose IS NOT NULL",
        group_by = 'purpose',
        order_by = 'purpose'
    }
    
    -- Loop through each purpose and create a section
    if purposes and purposes.purpose then
        for i, purpose in ipairs(purposes.purpose) do
            html = html .. '<div class="video-section">\n'
            html = html .. '  <div class="video-section-header">\n'
            html = html .. '    <h3 class="section-title">' .. purpose .. '</h3>\n'
            html = html .. '  </div>\n'
            html = html .. '  <div class="video-grid">\n'
            
            -- Get videos for this purpose
            local videos = mw.ext.externalData.getExternalData{
                source = 'externaldb',
                from = 'data_videos',
                data = 'video_id=video_id,youtube_id=youtube_id,title=title,channel_title=channel_title,channel_id=channel_id,author=author,published_at=published_at,duration_sec=duration_sec,purpose=purpose,primary_tag=primary_tag,secondary_tag=secondary_tag,description=description,video_notes=video_notes,video_internal_link=video_internal_link',
                where = "primary_tag='" .. category .. "' AND purpose='" .. purpose .. "' AND visibility='public'",
                order_by = 'published_at DESC'
            }
            
            -- Render each video card
            if videos and videos.video_id then
                for j, video_id in ipairs(videos.video_id) do
                    local duration = formatDuration(tonumber(videos.duration_sec[j] or 0))
                    
                    html = html .. '    <div class="video-card" '
                    html = html .. 'data-video-id="' .. video_id .. '" '
                    html = html .. 'data-youtube-id="' .. (videos.youtube_id[j] or '') .. '" '
                    html = html .. 'data-title="' .. escapeQuotes(videos.title[j] or '') .. '" '
                    html = html .. 'data-channel="' .. escapeQuotes(videos.channel_title[j] or '') .. '" '
                    html = html .. 'data-channel-id="' .. (videos.channel_id[j] or '') .. '" '
                    html = html .. 'data-author="' .. escapeQuotes(videos.author[j] or '') .. '" '
                    html = html .. 'data-published="' .. (videos.published_at[j] or '') .. '" '
                    html = html .. 'data-duration="' .. (videos.duration_sec[j] or '') .. '" '
                    html = html .. 'data-purpose="' .. escapeQuotes(videos.purpose[j] or '') .. '" '
                    html = html .. 'data-primary-tag="' .. (videos.primary_tag[j] or '') .. '" '
                    html = html .. 'data-secondary-tag="' .. (videos.secondary_tag[j] or '') .. '" '
                    html = html .. 'data-description="' .. escapeQuotes(videos.description[j] or '') .. '" '
                    html = html .. 'data-notes="' .. escapeQuotes(videos.video_notes[j] or '') .. '" '
                    html = html .. 'data-internal-link="' .. escapeQuotes(videos.video_internal_link[j] or '') .. '">\n'
                    
                    html = html .. '      <div class="video-thumbnail" style="background-image: url(\'https://img.youtube.com/vi/' .. (videos.youtube_id[j] or '') .. '/mqdefault.jpg\');">\n'
                    if duration ~= '' then
                        html = html .. '        <span class="video-duration">' .. duration .. '</span>\n'
                    end
                    html = html .. '      </div>\n'
                    html = html .. '      <div class="video-info">\n'
                    html = html .. '        <div class="video-title">' .. (videos.title[j] or '') .. '</div>\n'
                    html = html .. '        <div class="video-channel">' .. (videos.channel_title[j] or '') .. '</div>\n'
                    html = html .. '      </div>\n'
                    html = html .. '    </div>\n'
                end
            end
            
            html = html .. '  </div>\n'
            html = html .. '</div>\n'
        end
    end
    
    return html
end

-- Render featured videos (shows videos from all categories)
function p.renderFeaturedVideos(frame)
    local html = ''
    
    -- Get unique purposes from all categories
    local purposes = mw.ext.externalData.getExternalData{
        source = 'externaldb',
        from = 'data_videos',
        data = 'purpose=purpose',
        where = "visibility='public' AND purpose IS NOT NULL",
        group_by = 'purpose',
        order_by = 'purpose',
        limit = 10  -- Limit featured to first 10 purposes
    }
    
    -- Loop through each purpose and create a section
    if purposes and purposes.purpose then
        for i, purpose in ipairs(purposes.purpose) do
            html = html .. '<div class="video-section">\n'
            html = html .. '  <div class="video-section-header">\n'
            html = html .. '    <h3 class="section-title">' .. purpose .. '</h3>\n'
            html = html .. '  </div>\n'
            html = html .. '  <div class="video-grid">\n'
            
            -- Get videos for this purpose
            local videos = mw.ext.externalData.getExternalData{
                source = 'externaldb',
                from = 'data_videos',
                data = 'video_id=video_id,youtube_id=youtube_id,title=title,channel_title=channel_title,channel_id=channel_id,author=author,published_at=published_at,duration_sec=duration_sec,purpose=purpose,primary_tag=primary_tag,secondary_tag=secondary_tag,description=description,video_notes=video_notes,video_internal_link=video_internal_link',
                where = "purpose='" .. purpose .. "' AND visibility='public'",
                order_by = 'published_at DESC',
                limit = 4  -- Limit to 4 videos per purpose in featured
            }
            
            -- Render each video card
            if videos and videos.video_id then
                for j, video_id in ipairs(videos.video_id) do
                    local duration = formatDuration(tonumber(videos.duration_sec[j] or 0))
                    
                    html = html .. '    <div class="video-card" '
                    html = html .. 'data-video-id="' .. video_id .. '" '
                    html = html .. 'data-youtube-id="' .. (videos.youtube_id[j] or '') .. '" '
                    html = html .. 'data-title="' .. escapeQuotes(videos.title[j] or '') .. '" '
                    html = html .. 'data-channel="' .. escapeQuotes(videos.channel_title[j] or '') .. '" '
                    html = html .. 'data-channel-id="' .. (videos.channel_id[j] or '') .. '" '
                    html = html .. 'data-author="' .. escapeQuotes(videos.author[j] or '') .. '" '
                    html = html .. 'data-published="' .. (videos.published_at[j] or '') .. '" '
                    html = html .. 'data-duration="' .. (videos.duration_sec[j] or '') .. '" '
                    html = html .. 'data-purpose="' .. escapeQuotes(videos.purpose[j] or '') .. '" '
                    html = html .. 'data-primary-tag="' .. (videos.primary_tag[j] or '') .. '" '
                    html = html .. 'data-secondary-tag="' .. (videos.secondary_tag[j] or '') .. '" '
                    html = html .. 'data-description="' .. escapeQuotes(videos.description[j] or '') .. '" '
                    html = html .. 'data-notes="' .. escapeQuotes(videos.video_notes[j] or '') .. '" '
                    html = html .. 'data-internal-link="' .. escapeQuotes(videos.video_internal_link[j] or '') .. '">\n'
                    
                    html = html .. '      <div class="video-thumbnail" style="background-image: url(\'https://img.youtube.com/vi/' .. (videos.youtube_id[j] or '') .. '/mqdefault.jpg\');">\n'
                    if duration ~= '' then
                        html = html .. '        <span class="video-duration">' .. duration .. '</span>\n'
                    end
                    html = html .. '      </div>\n'
                    html = html .. '      <div class="video-info">\n'
                    html = html .. '        <div class="video-title">' .. (videos.title[j] or '') .. '</div>\n'
                    html = html .. '        <div class="video-channel">' .. (videos.channel_title[j] or '') .. '</div>\n'
                    html = html .. '      </div>\n'
                    html = html .. '    </div>\n'
                end
            end
            
            html = html .. '  </div>\n'
            html = html .. '</div>\n'
        end
    end
    
    return html
end

return p